diff --git a/Makefile b/Makefile index 9645fbd3a..6027aa303 100644 --- a/Makefile +++ b/Makefile @@ -90,7 +90,6 @@ post-install: @sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json @sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json @sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json - @sed -i'' -e "s|super.onBackPressed();|this.moveTaskToBack(true);|g" node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/controllers/NavigationActivity.java @sed -i'' -e "s|compile 'com.facebook.react:react-native:0.17.+'|compile 'com.facebook.react:react-native:+'|g" node_modules/react-native-bottom-sheet/android/build.gradle @if [ $(shell grep "const Platform" node_modules/react-native/Libraries/Lists/VirtualizedList.js | grep -civ grep) -eq 0 ]; then \ sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \ @@ -161,7 +160,7 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap @if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \ echo Starting React Native packager server; \ npm start & echo Running Android app in development; \ - if [ ! -z ${VARIANT} ]; then \ + if [ ! -z ${VARIANT} ]; then \ react-native run-android --no-packager --variant=${VARIANT}; \ else \ react-native run-android --no-packager; \ diff --git a/android/app/build.gradle b/android/app/build.gradle index a5be18151..583ea5752 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -122,6 +122,7 @@ android { applicationId "com.mattermost.rnbeta" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion + missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5" versionCode 207 versionName "1.21.0" multiDexEnabled = true diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java b/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java index b190dad0c..023da9c70 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java @@ -2,12 +2,14 @@ package com.mattermost.rnbeta; import android.os.Bundle; import android.support.annotation.Nullable; -import com.reactnativenavigation.controllers.SplashActivity; -public class MainActivity extends SplashActivity { +import com.reactnativenavigation.NavigationActivity; + +public class MainActivity extends NavigationActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setContentView(R.layout.launch_screen); /** * Reference: https://stackoverflow.com/questions/7944338/resume-last-activity-when-launcher-icon-is-clicked @@ -23,9 +25,4 @@ public class MainActivity extends SplashActivity { return; } } - - @Override - public int getSplashLayout() { - return R.layout.launch_screen; - } } diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java index 4ac35a02e..eb952447d 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -5,12 +5,26 @@ import com.mattermost.share.RealPathUtil; import android.app.Activity; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.content.Context; +import android.content.RestrictionsManager; import android.os.Bundle; +import android.util.Log; + import java.io.File; import java.util.Arrays; import java.util.List; +import com.facebook.soloader.SoLoader; +import com.facebook.react.ReactPackage; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactMarker; +import com.facebook.react.bridge.ReactMarkerConstants; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + import com.reactnativedocumentpicker.ReactNativeDocumentPicker; import com.oblador.keychain.KeychainPackage; import com.reactlibrary.RNReactNativeDocViewerPackage; @@ -26,10 +40,6 @@ import com.reactnativecommunity.asyncstorage.AsyncStoragePackage; import com.reactnativecommunity.netinfo.NetInfoPackage; import com.reactnativecommunity.webview.RNCWebViewPackage; import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; - -import com.facebook.react.ReactPackage; -import com.facebook.soloader.SoLoader; - import com.imagepicker.ImagePickerPackage; import com.gnet.bottomsheet.RNBottomSheetPackage; import com.learnium.RNDeviceInfo.RNDeviceInfo; @@ -37,6 +47,8 @@ import com.psykar.cookiemanager.CookieManagerPackage; import com.oblador.vectoricons.VectorIconsPackage; import com.BV.LinearGradient.LinearGradientPackage; import com.reactnativenavigation.NavigationApplication; +import com.reactnativenavigation.react.NavigationReactNativeHost; +import com.reactnativenavigation.react.ReactGateway; import com.wix.reactnativenotifications.RNNotificationsPackage; import com.wix.reactnativenotifications.core.notification.INotificationsApplication; import com.wix.reactnativenotifications.core.notification.IPushNotification; @@ -46,19 +58,9 @@ import com.wix.reactnativenotifications.core.AppLaunchHelper; import com.wix.reactnativenotifications.core.AppLifecycleFacade; import com.wix.reactnativenotifications.core.JsIOHelper; -import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReactMarker; -import com.facebook.react.bridge.ReactMarkerConstants; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; -import android.support.annotation.Nullable; - -import android.util.Log; - public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication { - public NotificationsLifecycleFacade notificationsLifecycleFacade; + public static MainApplication instance; + public Boolean sharedExtensionIsOpened = false; public long APP_START_TIME; @@ -69,6 +71,19 @@ public class MainApplication extends NavigationApplication implements INotificat public long PROCESS_PACKAGES_START; public long PROCESS_PACKAGES_END; + private Bundle mManagedConfig = null; + + @Override + protected ReactGateway createReactGateway() { + ReactNativeHost host = new NavigationReactNativeHost(this, isDebug(), createAdditionalReactPackages()) { + @Override + protected String getJSMainModuleName() { + return "index"; + } + }; + return new ReactGateway(this, isDebug(), host); + } + @Override public boolean isDebug() { return BuildConfig.DEBUG; @@ -107,45 +122,30 @@ public class MainApplication extends NavigationApplication implements INotificat ); } - @Override - public String getJSMainModuleName() { - return "index"; - } - @Override public void onCreate() { super.onCreate(); instance = this; + registerActivityLifecycleCallbacks(new ManagedActivityLifecycleCallbacks()); + // Delete any previous temp files created by the app File tempFolder = new File(getApplicationContext().getCacheDir(), "mmShare"); RealPathUtil.deleteTempFiles(tempFolder); Log.i("ReactNative", "Cleaning temp cache " + tempFolder.getAbsolutePath()); - // Create an object of the custom facade impl - notificationsLifecycleFacade = NotificationsLifecycleFacade.getInstance(); - // Attach it to react-native-navigation - setActivityCallbacks(notificationsLifecycleFacade); - SoLoader.init(this, /* native exopackage */ false); // Uncomment to listen to react markers for build that has telemetry enabled // addReactMarkerListener(); } - @Override - public boolean clearHostOnActivityDestroy(Activity activity) { - // This solves the issue where the splash screen does not go away - // after the app is killed by the OS cause of memory or a long time in the background - return false; - } - @Override public IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade defaultFacade, AppLaunchHelper defaultAppLaunchHelper) { return new CustomPushNotification( context, bundle, - notificationsLifecycleFacade, // Instead of defaultFacade!!! + defaultFacade, defaultAppLaunchHelper, new JsIOHelper() ); @@ -156,6 +156,51 @@ public class MainApplication extends NavigationApplication implements INotificat return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper); } + public ReactContext getRunningReactContext() { + final ReactGateway reactGateway = getReactGateway(); + + if (reactGateway == null) { + return null; + } + + return reactGateway + .getReactNativeHost() + .getReactInstanceManager() + .getCurrentReactContext(); + } + + public synchronized Bundle loadManagedConfig(Context ctx) { + if (ctx != null) { + RestrictionsManager myRestrictionsMgr = + (RestrictionsManager) ctx.getSystemService(Context.RESTRICTIONS_SERVICE); + + mManagedConfig = myRestrictionsMgr.getApplicationRestrictions(); + myRestrictionsMgr = null; + + if (mManagedConfig!= null && mManagedConfig.size() > 0) { + return mManagedConfig; + } + + return null; + } + + return null; + } + + public synchronized Bundle getManagedConfig() { + if (mManagedConfig!= null && mManagedConfig.size() > 0) { + return mManagedConfig; + } + + ReactContext ctx = getRunningReactContext(); + + if (ctx != null) { + return loadManagedConfig(ctx); + } + + return null; + } + private void addReactMarkerListener() { ReactMarker.addListener(new ReactMarker.MarkerListener() { @Override @@ -169,7 +214,7 @@ public class MainApplication extends NavigationApplication implements INotificat PROCESS_PACKAGES_END = System.currentTimeMillis(); } else if (name.toString() == ReactMarkerConstants.CONTENT_APPEARED.toString()) { CONTENT_APPEARED = System.currentTimeMillis(); - ReactContext ctx = getReactGateway().getReactContext(); + ReactContext ctx = getRunningReactContext(); if (ctx != null) { WritableMap map = Arguments.createMap(); diff --git a/android/app/src/main/java/com/mattermost/rnbeta/ManagedActivityLifecycleCallbacks.java b/android/app/src/main/java/com/mattermost/rnbeta/ManagedActivityLifecycleCallbacks.java new file mode 100644 index 000000000..cebf7a187 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/ManagedActivityLifecycleCallbacks.java @@ -0,0 +1,145 @@ +package com.mattermost.rnbeta; + +import android.os.Bundle; +import android.app.Activity; +import android.app.Application.ActivityLifecycleCallbacks; +import android.content.Context; +import android.content.RestrictionsManager; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.IntentFilter; +import android.view.WindowManager; +import android.view.WindowManager.LayoutParams; +import android.util.ArraySet; +import android.util.Log; + +import java.util.Set; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +public class ManagedActivityLifecycleCallbacks implements ActivityLifecycleCallbacks { + private static final String TAG = ManagedActivityLifecycleCallbacks.class.getSimpleName(); + + private final IntentFilter restrictionsFilter = + new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED); + + private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context ctx, Intent intent) { + if (ctx != null) { + Bundle managedConfig = MainApplication.instance.loadManagedConfig(ctx); + + // Check current configuration settings, change your app's UI and + // functionality as necessary. + Log.i(TAG, "Managed Configuration Changed"); + sendConfigChanged(managedConfig); + } + } + }; + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + MattermostManagedModule managedModule = MattermostManagedModule.getInstance(); + if (managedModule != null && managedModule.isBlurAppScreenEnabled() && activity != null) { + activity.getWindow().setFlags(LayoutParams.FLAG_SECURE, + LayoutParams.FLAG_SECURE); + } + + Bundle managedConfig = MainApplication.instance.getManagedConfig(); + if (managedConfig != null && activity != null) { + activity.registerReceiver(restrictionsReceiver, restrictionsFilter); + } + } + + @Override + public void onActivityResumed(Activity activity) { + ReactContext ctx = MainApplication.instance.getRunningReactContext(); + Bundle managedConfig = MainApplication.instance.getManagedConfig(); + + if (ctx != null) { + Bundle newConfig = MainApplication.instance.loadManagedConfig(ctx); + if (!equalBundles(newConfig, managedConfig)) { + Log.i(TAG, "onResumed Managed Configuration Changed"); + sendConfigChanged(newConfig); + } + } + } + + @Override + public void onActivityStopped(Activity activity) { + Bundle managedConfig = MainApplication.instance.getManagedConfig(); + + if (managedConfig != null) { + try { + activity.unregisterReceiver(restrictionsReceiver); + } catch (IllegalArgumentException e) { + // Just ignore this cause the receiver wasn't registered for this activity + } + } + } + + @Override + public void onActivityStarted(Activity activity) { + } + + @Override + public void onActivityPaused(Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + public void onActivityDestroyed(Activity activity) { + } + + private void sendConfigChanged(Bundle config) { + WritableMap result = Arguments.createMap(); + if (config != null) { + result = Arguments.fromBundle(config); + } + ReactContext ctx = MainApplication.instance.getRunningReactContext(); + + if (ctx != null) { + ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("managedConfigDidChange", result); + } + } + + private boolean equalBundles(Bundle one, Bundle two) { + if (one == null || two == null) + return false; + + if(one.size() != two.size()) + return false; + + Set setOne = new ArraySet(); + setOne.addAll(one.keySet()); + setOne.addAll(two.keySet()); + Object valueOne; + Object valueTwo; + + for(String key : setOne) { + if (!one.containsKey(key) || !two.containsKey(key)) + return false; + + valueOne = one.get(key); + valueTwo = two.get(key); + if(valueOne instanceof Bundle && valueTwo instanceof Bundle && + !equalBundles((Bundle) valueOne, (Bundle) valueTwo)) { + return false; + } + else if(valueOne == null) { + if(valueTwo != null) + return false; + } + else if(!valueOne.equals(valueTwo)) + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java index bbc0a8ef0..68ecbec2a 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java @@ -53,7 +53,7 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule { @ReactMethod public void getConfig(final Promise promise) { try { - Bundle config = NotificationsLifecycleFacade.getInstance().getManagedConfig(); + Bundle config = MainApplication.instance.getManagedConfig(); if (config != null) { Object result = Arguments.fromBundle(config); diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationsLifecycleFacade.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationsLifecycleFacade.java deleted file mode 100644 index f6177a2d9..000000000 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationsLifecycleFacade.java +++ /dev/null @@ -1,251 +0,0 @@ -package com.mattermost.rnbeta; - -import android.app.Activity; -import android.content.pm.ActivityInfo; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.RestrictionsManager; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.util.Log; -import android.util.ArraySet; -import android.view.WindowManager; -import android.view.WindowManager.LayoutParams; -import android.content.res.Configuration; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.modules.core.DeviceEventManagerModule; - -import com.reactnativenavigation.NavigationApplication; -import com.reactnativenavigation.controllers.ActivityCallbacks; -import com.reactnativenavigation.react.ReactGateway; -import com.wix.reactnativenotifications.core.AppLifecycleFacade; - -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; - - -public class NotificationsLifecycleFacade extends ActivityCallbacks implements AppLifecycleFacade { - private static final String TAG = NotificationsLifecycleFacade.class.getSimpleName(); - private static NotificationsLifecycleFacade instance; - - private Bundle managedConfig = null; - private Activity mVisibleActivity; - private Set mListeners = new CopyOnWriteArraySet<>(); - - private final IntentFilter restrictionsFilter = - new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED); - - private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() { - @Override public void onReceive(Context context, Intent intent) { - if (context != null) { - // Get the current configuration bundle - RestrictionsManager myRestrictionsMgr = - (RestrictionsManager) context - .getSystemService(Context.RESTRICTIONS_SERVICE); - managedConfig = myRestrictionsMgr.getApplicationRestrictions(); - - // Check current configuration settings, change your app's UI and - // functionality as necessary. - Log.i("ReactNative", "Managed Configuration Changed"); - sendConfigChanged(managedConfig); - } - } - }; - - public static NotificationsLifecycleFacade getInstance() { - if (instance == null) { - instance = new NotificationsLifecycleFacade(); - } - - return instance; - } - - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - MattermostManagedModule managedModule = MattermostManagedModule.getInstance(); - if (managedModule != null && managedModule.isBlurAppScreenEnabled() && activity != null) { - activity.getWindow().setFlags(LayoutParams.FLAG_SECURE, - LayoutParams.FLAG_SECURE); - } - if (managedConfig != null && managedConfig.size() > 0 && activity != null) { - activity.registerReceiver(restrictionsReceiver, restrictionsFilter); - } - - if (activity != null) { - activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - } - } - - @Override - public void onActivityResumed(Activity activity) { - switchToVisible(activity); - - ReactContext ctx = getRunningReactContext(); - if (managedConfig != null && managedConfig.size() > 0 && ctx != null) { - - RestrictionsManager myRestrictionsMgr = - (RestrictionsManager) ctx - .getSystemService(Context.RESTRICTIONS_SERVICE); - - Bundle newConfig = myRestrictionsMgr.getApplicationRestrictions(); - if (!equalBundles(newConfig ,managedConfig)) { - Log.i("ReactNative", "onResumed Managed Configuration Changed"); - managedConfig = newConfig; - sendConfigChanged(managedConfig); - } - } - } - - @Override - public void onActivityPaused(Activity activity) { - switchToInvisible(activity); - } - - @Override - public void onActivityStopped(Activity activity) { - switchToInvisible(activity); - if (managedConfig != null && managedConfig.size() > 0) { - try { - activity.unregisterReceiver(restrictionsReceiver); - } catch (IllegalArgumentException e) { - // Just ignore this cause the receiver wasn't registered for this activity - } - } - } - - @Override - public void onActivityDestroyed(Activity activity) { - switchToInvisible(activity); - } - - @Override - public boolean isReactInitialized() { - return NavigationApplication.instance.isReactContextInitialized(); - } - - @Override - public ReactContext getRunningReactContext() { - final ReactGateway reactGateway = NavigationApplication.instance.getReactGateway(); - if (reactGateway == null || !reactGateway.isInitialized()) { - return null; - } - - return reactGateway.getReactContext(); - } - - @Override - public boolean isAppVisible() { - return mVisibleActivity != null; - } - - @Override - public synchronized void addVisibilityListener(AppVisibilityListener listener) { - mListeners.add(listener); - } - - @Override - public synchronized void removeVisibilityListener(AppVisibilityListener listener) { - mListeners.remove(listener); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - if (mVisibleActivity != null) { - Intent intent = new Intent("onConfigurationChanged"); - intent.putExtra("newConfig", newConfig); - mVisibleActivity.sendBroadcast(intent); - } - } - - private synchronized void switchToVisible(Activity activity) { - if (mVisibleActivity == null) { - mVisibleActivity = activity; - Log.v(TAG, "Activity is now visible ("+activity+")"); - for (AppVisibilityListener listener : mListeners) { - listener.onAppVisible(); - } - } - } - - private synchronized void switchToInvisible(Activity activity) { - if (mVisibleActivity == activity) { - mVisibleActivity = null; - Log.v(TAG, "Activity is now NOT visible ("+activity+")"); - for (AppVisibilityListener listener : mListeners) { - listener.onAppNotVisible(); - } - } - } - - public synchronized void LoadManagedConfig(ReactContext ctx) { - if (ctx != null) { - RestrictionsManager myRestrictionsMgr = - (RestrictionsManager) ctx - .getSystemService(Context.RESTRICTIONS_SERVICE); - - managedConfig = myRestrictionsMgr.getApplicationRestrictions(); - myRestrictionsMgr = null; - } - } - - public synchronized Bundle getManagedConfig() { - if (managedConfig!= null && managedConfig.size() > 0) { - return managedConfig; - } - - ReactContext ctx = getRunningReactContext(); - - if (ctx != null) { - LoadManagedConfig(ctx); - return managedConfig; - } - - return null; - } - - public void sendConfigChanged(Bundle config) { - Object result = Arguments.fromBundle(config); - ReactContext ctx = getRunningReactContext(); - if (ctx != null) { - ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class). - emit("managedConfigDidChange", result); - } - } - - private boolean equalBundles(Bundle one, Bundle two) { - if (one == null || two == null) - return false; - - if(one.size() != two.size()) - return false; - - Set setOne = new ArraySet(); - setOne.addAll(one.keySet()); - setOne.addAll(two.keySet()); - Object valueOne; - Object valueTwo; - - for(String key : setOne) { - if (!one.containsKey(key) || !two.containsKey(key)) - return false; - - valueOne = one.get(key); - valueTwo = two.get(key); - if(valueOne instanceof Bundle && valueTwo instanceof Bundle && - !equalBundles((Bundle) valueOne, (Bundle) valueTwo)) { - return false; - } - else if(valueOne == null) { - if(valueTwo != null) - return false; - } - else if(!valueOne.equals(valueTwo)) - return false; - } - - return true; - } -} diff --git a/android/build.gradle b/android/build.gradle index a9beff6ed..10f12f4b7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -11,6 +11,8 @@ buildscript { repositories { jcenter() google() + mavenLocal() + mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:3.3.1' @@ -37,6 +39,7 @@ subprojects { allprojects { repositories { google() + mavenCentral() mavenLocal() jcenter() maven { @@ -47,5 +50,8 @@ allprojects { // Local Maven repo containing AARs with JSC library built for Android url "$rootDir/../node_modules/jsc-android/dist" } + maven { + url "https://jitpack.io" + } } } diff --git a/android/settings.gradle b/android/settings.gradle index 185f7b892..a07602e25 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -22,7 +22,7 @@ project(':jail-monkey').projectDir = new File(rootProject.projectDir, '../node_m include ':react-native-local-auth' project(':react-native-local-auth').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-local-auth/android') include ':react-native-navigation' -project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/') +project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/lib/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-bottom-sheet' diff --git a/app/actions/navigation.js b/app/actions/navigation.js new file mode 100644 index 000000000..f1e6852bb --- /dev/null +++ b/app/actions/navigation.js @@ -0,0 +1,361 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Platform} from 'react-native'; +import {Navigation} from 'react-native-navigation'; + +import merge from 'deepmerge'; + +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; + +import EphemeralStore from 'app/store/ephemeral_store'; + +export function resetToChannel(passProps = {}) { + return (dispatch, getState) => { + const theme = getTheme(getState()); + + Navigation.setRoot({ + root: { + stack: { + children: [{ + component: { + name: 'Channel', + passProps, + options: { + layout: { + backgroundColor: 'transparent', + }, + statusBar: { + visible: true, + }, + topBar: { + visible: false, + height: 0, + backButton: { + color: theme.sidebarHeaderTextColor, + title: '', + }, + background: { + color: theme.sidebarHeaderBg, + }, + title: { + color: theme.sidebarHeaderTextColor, + }, + }, + }, + }, + }], + }, + }, + }); + }; +} + +export function resetToSelectServer(allowOtherServers) { + return (dispatch, getState) => { + const theme = getTheme(getState()); + + Navigation.setRoot({ + root: { + stack: { + children: [{ + component: { + name: 'SelectServer', + passProps: { + allowOtherServers, + }, + options: { + statusBar: { + visible: true, + }, + topBar: { + backButton: { + color: theme.sidebarHeaderTextColor, + title: '', + }, + background: { + color: theme.sidebarHeaderBg, + }, + visible: false, + height: 0, + }, + }, + }, + }], + }, + }, + }); + }; +} + +export function resetToTeams(name, title, passProps = {}, options = {}) { + return (dispatch, getState) => { + const theme = getTheme(getState()); + const defaultOptions = { + layout: { + backgroundColor: theme.centerChannelBg, + }, + statusBar: { + visible: true, + }, + topBar: { + visible: true, + title: { + color: theme.sidebarHeaderTextColor, + text: title, + }, + backButton: { + color: theme.sidebarHeaderTextColor, + title: '', + }, + background: { + color: theme.sidebarHeaderBg, + }, + }, + }; + + Navigation.setRoot({ + root: { + stack: { + children: [{ + component: { + name, + passProps, + options: merge(defaultOptions, options), + }, + }], + }, + }, + }); + }; +} + +export function goToScreen(name, title, passProps = {}, options = {}) { + return (dispatch, getState) => { + const state = getState(); + const componentId = EphemeralStore.getTopComponentId(); + const theme = getTheme(state); + const defaultOptions = { + layout: { + backgroundColor: theme.centerChannelBg, + }, + topBar: { + animate: true, + visible: true, + backButton: { + color: theme.sidebarHeaderTextColor, + title: '', + }, + background: { + color: theme.sidebarHeaderBg, + }, + title: { + color: theme.sidebarHeaderTextColor, + text: title, + }, + }, + }; + + Navigation.push(componentId, { + component: { + name, + passProps, + options: merge(defaultOptions, options), + }, + }); + }; +} + +export function popTopScreen() { + return () => { + const componentId = EphemeralStore.getTopComponentId(); + + Navigation.pop(componentId); + }; +} + +export function popToRoot() { + return () => { + const componentId = EphemeralStore.getTopComponentId(); + + Navigation.popToRoot(componentId).catch(() => { + // RNN returns a promise rejection if there are no screens + // atop the root screen to pop. We'll do nothing in this + // case but we will catch the rejection here so that the + // caller doesn't have to. + }); + }; +} + +export function showModal(name, title, passProps = {}, options = {}) { + return (dispatch, getState) => { + const theme = getTheme(getState()); + const defaultOptions = { + layout: { + backgroundColor: theme.centerChannelBg, + }, + statusBar: { + visible: true, + }, + topBar: { + animate: true, + visible: true, + backButton: { + color: theme.sidebarHeaderTextColor, + title: '', + }, + background: { + color: theme.sidebarHeaderBg, + }, + title: { + color: theme.sidebarHeaderTextColor, + text: title, + }, + leftButtonColor: theme.sidebarHeaderTextColor, + rightButtonColor: theme.sidebarHeaderTextColor, + }, + }; + + Navigation.showModal({ + stack: { + children: [{ + component: { + name, + passProps, + options: merge(defaultOptions, options), + }, + }], + }, + }); + }; +} + +export function showModalOverCurrentContext(name, passProps = {}, options = {}) { + return (dispatch) => { + const title = ''; + const animationsEnabled = (Platform.OS === 'android').toString(); + const defaultOptions = { + modalPresentationStyle: 'overCurrentContext', + layout: { + backgroundColor: 'transparent', + }, + topBar: { + visible: false, + height: 0, + }, + animations: { + showModal: { + enabled: animationsEnabled, + alpha: { + from: 0, + to: 1, + duration: 250, + }, + }, + dismissModal: { + enabled: animationsEnabled, + alpha: { + from: 1, + to: 0, + duration: 250, + }, + }, + }, + }; + const mergeOptions = merge(defaultOptions, options); + + dispatch(showModal(name, title, passProps, mergeOptions)); + }; +} + +export function showSearchModal(initialValue = '') { + return (dispatch) => { + const name = 'Search'; + const title = ''; + const passProps = {initialValue}; + const options = { + topBar: { + visible: false, + height: 0, + }, + }; + + dispatch(showModal(name, title, passProps, options)); + }; +} + +export function dismissModal(options = {}) { + return () => { + const componentId = EphemeralStore.getTopComponentId(); + + Navigation.dismissModal(componentId, options); + }; +} + +export function dismissAllModals(options = {}) { + return () => { + Navigation.dismissAllModals(options).catch(() => { + // RNN returns a promise rejection if there are no modals to + // dismiss. We'll do nothing in this case but we will catch + // the rejection here so that the caller doesn't have to. + }); + }; +} + +export function peek(name, passProps = {}, options = {}) { + return () => { + const componentId = EphemeralStore.getTopComponentId(); + const defaultOptions = { + preview: { + commit: false, + }, + }; + + Navigation.push(componentId, { + component: { + name, + passProps, + options: merge(defaultOptions, options), + }, + }); + }; +} + +export function setButtons(componentId, buttons = {leftButtons: [], rightButtons: []}) { + return () => { + Navigation.mergeOptions(componentId, { + topBar: { + ...buttons, + }, + }); + }; +} + +export function showOverlay(name, passProps, options = {}) { + return () => { + const defaultOptions = { + overlay: { + interceptTouchOutside: false, + }, + }; + + Navigation.showOverlay({ + component: { + name, + passProps, + options: merge(defaultOptions, options), + }, + }); + }; +} + +export function dismissOverlay(componentId) { + return () => { + return Navigation.dismissOverlay(componentId).catch(() => { + // RNN returns a promise rejection if there is no modal with + // this componentId to dismiss. We'll do nothing in this case + // but we will catch the rejection here so that the caller + // doesn't have to. + }); + }; +} diff --git a/app/actions/views/search.js b/app/actions/views/search.js index 2726e6635..2ccd0060d 100644 --- a/app/actions/views/search.js +++ b/app/actions/views/search.js @@ -1,8 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; - import {ViewTypes} from 'app/constants'; export function handleSearchDraftChanged(text) { @@ -13,25 +11,3 @@ export function handleSearchDraftChanged(text) { }, getState); }; } - -export function showSearchModal(navigator, initialValue = '') { - return (dispatch, getState) => { - const theme = getTheme(getState()); - - const options = { - screen: 'Search', - animated: true, - backButtonTitle: '', - overrideBackPress: true, - passProps: { - initialValue, - }, - navigatorStyle: { - navBarHidden: true, - screenBackgroundColor: theme.centerChannelBg, - }, - }; - - navigator.showModal(options); - }; -} diff --git a/app/components/__snapshots__/profile_picture_button.test.js.snap b/app/components/__snapshots__/profile_picture_button.test.js.snap index 3cba2c9bd..c4205e2cf 100644 --- a/app/components/__snapshots__/profile_picture_button.test.js.snap +++ b/app/components/__snapshots__/profile_picture_button.test.js.snap @@ -1,29 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`profile_picture_button should match snapshot 1`] = ` - `; diff --git a/app/components/announcement_banner/announcement_banner.js b/app/components/announcement_banner/announcement_banner.js index 3da97744b..67b4b6d91 100644 --- a/app/components/announcement_banner/announcement_banner.js +++ b/app/components/announcement_banner/announcement_banner.js @@ -18,12 +18,14 @@ const {View: AnimatedView} = Animated; export default class AnnouncementBanner extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, bannerColor: PropTypes.string, bannerDismissed: PropTypes.bool, bannerEnabled: PropTypes.bool, bannerText: PropTypes.string, bannerTextColor: PropTypes.string, - navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, }; @@ -52,23 +54,16 @@ export default class AnnouncementBanner extends PureComponent { } handlePress = () => { - const {navigator, theme} = this.props; + const {actions} = this.props; + const {intl} = this.context; - navigator.push({ - screen: 'ExpandedAnnouncementBanner', - title: this.context.intl.formatMessage({ - id: 'mobile.announcement_banner.title', - defaultMessage: 'Announcement', - }), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, + const screen = 'ExpandedAnnouncementBanner'; + const title = intl.formatMessage({ + id: 'mobile.announcement_banner.title', + defaultMessage: 'Announcement', }); + + actions.goToScreen(screen, title); }; toggleBanner = (show = true) => { diff --git a/app/components/announcement_banner/announcement_banner.test.js b/app/components/announcement_banner/announcement_banner.test.js index 5312962ba..73085ebf1 100644 --- a/app/components/announcement_banner/announcement_banner.test.js +++ b/app/components/announcement_banner/announcement_banner.test.js @@ -12,12 +12,14 @@ jest.useFakeTimers(); describe('AnnouncementBanner', () => { const baseProps = { + actions: { + goToScreen: jest.fn(), + }, bannerColor: '#ddd', bannerDismissed: false, bannerEnabled: true, bannerText: 'Banner Text', bannerTextColor: '#fff', - navigator: {}, theme: Preferences.THEMES.default, }; diff --git a/app/components/announcement_banner/index.js b/app/components/announcement_banner/index.js index f560c461d..f06d6eea1 100644 --- a/app/components/announcement_banner/index.js +++ b/app/components/announcement_banner/index.js @@ -1,11 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {goToScreen} from 'app/actions/navigation'; + import AnnouncementBanner from './announcement_banner'; function mapStateToProps(state) { @@ -23,4 +26,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(AnnouncementBanner); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementBanner); diff --git a/app/components/at_mention/at_mention.js b/app/components/at_mention/at_mention.js index e096c5e1b..d34bae8ff 100644 --- a/app/components/at_mention/at_mention.js +++ b/app/components/at_mention/at_mention.js @@ -3,7 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {Clipboard, Platform, Text} from 'react-native'; +import {Clipboard, Text} from 'react-native'; import {intlShape} from 'react-intl'; import {displayUsername} from 'mattermost-redux/utils/user_utils'; @@ -14,10 +14,12 @@ import BottomSheet from 'app/utils/bottom_sheet'; export default class AtMention extends React.PureComponent { static propTypes = { + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, isSearchResult: PropTypes.bool, mentionName: PropTypes.string.isRequired, mentionStyle: CustomPropTypes.Style, - navigator: PropTypes.object.isRequired, onPostPress: PropTypes.func, textStyle: CustomPropTypes.Style, teammateNameDisplay: PropTypes.string, @@ -48,29 +50,15 @@ export default class AtMention extends React.PureComponent { } goToUserProfile = () => { - const {navigator, theme} = this.props; + const {actions} = this.props; const {intl} = this.context; - const options = { - screen: 'UserProfile', - title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}), - animated: true, - backButtonTitle: '', - passProps: { - userId: this.state.user.id, - }, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, + const screen = 'UserProfile'; + const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); + const passProps = { + userId: this.state.user.id, }; - if (Platform.OS === 'ios') { - navigator.push(options); - } else { - navigator.showModal(options); - } + actions.goToScreen(screen, title, passProps); }; getUserDetailsFromMentionName(props) { diff --git a/app/components/at_mention/index.js b/app/components/at_mention/index.js index 07ac8c733..857345ee5 100644 --- a/app/components/at_mention/index.js +++ b/app/components/at_mention/index.js @@ -1,12 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users'; import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {goToScreen} from 'app/actions/navigation'; + import AtMention from './at_mention'; function mapStateToProps(state) { @@ -17,4 +20,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(AtMention); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(AtMention); diff --git a/app/components/__snapshots__/attachment_button.test.js.snap b/app/components/attachment_button/__snapshots__/attachment_button.test.js.snap similarity index 100% rename from app/components/__snapshots__/attachment_button.test.js.snap rename to app/components/attachment_button/__snapshots__/attachment_button.test.js.snap diff --git a/app/components/attachment_button.js b/app/components/attachment_button/attachment_button.js similarity index 91% rename from app/components/attachment_button.js rename to app/components/attachment_button/attachment_button.js index 29bd3d0d0..07a3257c0 100644 --- a/app/components/attachment_button.js +++ b/app/components/attachment_button/attachment_button.js @@ -27,6 +27,9 @@ const ShareExtension = NativeModules.MattermostShare; export default class AttachmentButton extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + showModalOverCurrentContext: PropTypes.func.isRequired, + }).isRequired, blurTextBox: PropTypes.func.isRequired, browseFileTypes: PropTypes.string, validMimeTypes: PropTypes.array, @@ -39,7 +42,6 @@ export default class AttachmentButton extends PureComponent { fileCount: PropTypes.number, maxFileCount: PropTypes.number.isRequired, maxFileSize: PropTypes.number.isRequired, - navigator: PropTypes.object.isRequired, onShowFileMaxWarning: PropTypes.func, onShowFileSizeWarning: PropTypes.func, onShowUnsupportedMimeTypeWarning: PropTypes.func, @@ -340,21 +342,6 @@ export default class AttachmentButton extends PureComponent { } }; - handleFileAttachmentOption = (action) => { - this.props.navigator.dismissModal({ - animationType: 'none', - }); - - // Have to wait to launch the library attachment action. - // If we call the action after dismissModal with no delay then the - // Wix navigator will dismiss the library attachment modal as well. - setTimeout(() => { - if (typeof action === 'function') { - action(); - } - }, 100); - }; - showFileAttachmentOptions = () => { const { canBrowseFiles, @@ -366,6 +353,7 @@ export default class AttachmentButton extends PureComponent { maxFileCount, onShowFileMaxWarning, extraOptions, + actions, } = this.props; if (fileCount === maxFileCount) { @@ -378,7 +366,7 @@ export default class AttachmentButton extends PureComponent { if (canTakePhoto) { items.push({ - action: () => this.handleFileAttachmentOption(this.attachPhotoFromCamera), + action: this.attachPhotoFromCamera, text: { id: t('mobile.file_upload.camera_photo'), defaultMessage: 'Take Photo', @@ -389,7 +377,7 @@ export default class AttachmentButton extends PureComponent { if (canTakeVideo) { items.push({ - action: () => this.handleFileAttachmentOption(this.attachVideoFromCamera), + action: this.attachVideoFromCamera, text: { id: t('mobile.file_upload.camera_video'), defaultMessage: 'Take Video', @@ -400,7 +388,7 @@ export default class AttachmentButton extends PureComponent { if (canBrowsePhotoLibrary) { items.push({ - action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary), + action: this.attachFileFromLibrary, text: { id: t('mobile.file_upload.library'), defaultMessage: 'Photo Library', @@ -411,7 +399,7 @@ export default class AttachmentButton extends PureComponent { if (canBrowseVideoLibrary && Platform.OS === 'android') { items.push({ - action: () => this.handleFileAttachmentOption(this.attachVideoFromLibraryAndroid), + action: this.attachVideoFromLibraryAndroid, text: { id: t('mobile.file_upload.video'), defaultMessage: 'Video Library', @@ -422,7 +410,7 @@ export default class AttachmentButton extends PureComponent { if (canBrowseFiles) { items.push({ - action: () => this.handleFileAttachmentOption(this.attachFileFromFiles), + action: this.attachFileFromFiles, text: { id: t('mobile.file_upload.browse'), defaultMessage: 'Browse Files', @@ -439,21 +427,7 @@ export default class AttachmentButton extends PureComponent { }); } - this.props.navigator.showModal({ - screen: 'OptionsModal', - title: '', - animationType: 'none', - passProps: { - items, - }, - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - modalPresentationStyle: 'overCurrentContext', - }, - }); + actions.showModalOverCurrentContext('OptionsModal', {items}); }; render() { diff --git a/app/components/attachment_button.test.js b/app/components/attachment_button/attachment_button.test.js similarity index 96% rename from app/components/attachment_button.test.js rename to app/components/attachment_button/attachment_button.test.js index ae789c5c6..e513d3634 100644 --- a/app/components/attachment_button.test.js +++ b/app/components/attachment_button/attachment_button.test.js @@ -13,8 +13,10 @@ jest.mock('react-intl'); describe('AttachmentButton', () => { const baseProps = { + actions: { + showModalOverCurrentContext: jest.fn(), + }, theme: Preferences.THEMES.default, - navigator: {}, blurTextBox: jest.fn(), maxFileSize: 10, uploadFiles: jest.fn(), diff --git a/app/components/attachment_button/index.js b/app/components/attachment_button/index.js new file mode 100644 index 000000000..d48573136 --- /dev/null +++ b/app/components/attachment_button/index.js @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {showModalOverCurrentContext} from 'app/actions/navigation'; + +import AttachmentButton from './attachment_button'; + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + showModalOverCurrentContext, + }, dispatch), + }; +} + +export default connect(null, mapDispatchToProps)(AttachmentButton); diff --git a/app/components/autocomplete_selector/autocomplete_selector.js b/app/components/autocomplete_selector/autocomplete_selector.js index 49cbc44d6..2817da990 100644 --- a/app/components/autocomplete_selector/autocomplete_selector.js +++ b/app/components/autocomplete_selector/autocomplete_selector.js @@ -18,6 +18,7 @@ export default class AutocompleteSelector extends PureComponent { static propTypes = { actions: PropTypes.shape({ setAutocompleteSelector: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, }).isRequired, label: PropTypes.string, placeholder: PropTypes.string.isRequired, @@ -28,7 +29,6 @@ export default class AutocompleteSelector extends PureComponent { showRequiredAsterisk: PropTypes.bool, teammateNameDisplay: PropTypes.string, theme: PropTypes.object.isRequired, - navigator: PropTypes.object, onSelected: PropTypes.func, helpText: PropTypes.node, errorText: PropTypes.node, @@ -96,22 +96,12 @@ export default class AutocompleteSelector extends PureComponent { goToSelectorScreen = preventDoubleTap(() => { const {formatMessage} = this.context.intl; - const {navigator, theme, actions, dataSource, options, placeholder} = this.props; + const {actions, dataSource, options, placeholder} = this.props; + const screen = 'SelectorScreen'; + const title = placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'}); actions.setAutocompleteSelector(dataSource, this.handleSelect, options); - - navigator.push({ - backButtonTitle: '', - screen: 'SelectorScreen', - title: placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }); + actions.goToScreen(screen, title); }); render() { diff --git a/app/components/autocomplete_selector/index.js b/app/components/autocomplete_selector/index.js index dc77afea9..afb4c445c 100644 --- a/app/components/autocomplete_selector/index.js +++ b/app/components/autocomplete_selector/index.js @@ -6,6 +6,7 @@ import {connect} from 'react-redux'; import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {goToScreen} from 'app/actions/navigation'; import {setAutocompleteSelector} from 'app/actions/views/post'; import AutocompleteSelector from './autocomplete_selector'; @@ -21,6 +22,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ setAutocompleteSelector, + goToScreen, }, dispatch), }; } diff --git a/app/components/channel_intro/channel_intro.js b/app/components/channel_intro/channel_intro.js index d50e434d2..b9d0200ea 100644 --- a/app/components/channel_intro/channel_intro.js +++ b/app/components/channel_intro/channel_intro.js @@ -4,7 +4,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { - Platform, Text, TouchableOpacity, View, @@ -21,11 +20,13 @@ import {t} from 'app/utils/i18n'; class ChannelIntro extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, creator: PropTypes.object, currentChannel: PropTypes.object.isRequired, currentChannelMembers: PropTypes.array.isRequired, intl: intlShape.isRequired, - navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, }; @@ -34,28 +35,14 @@ class ChannelIntro extends PureComponent { }; goToUserProfile = (userId) => { - const {intl, navigator, theme} = this.props; - const options = { - 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, - }, + const {actions, intl} = this.props; + const screen = 'UserProfile'; + const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); + const passProps = { + userId, }; - if (Platform.OS === 'ios') { - navigator.push(options); - } else { - navigator.showModal(options); - } + actions.goToScreen(screen, title, passProps); }; getDisplayName = (member) => { diff --git a/app/components/channel_intro/index.js b/app/components/channel_intro/index.js index 5495a4d01..8d6ccf49e 100644 --- a/app/components/channel_intro/index.js +++ b/app/components/channel_intro/index.js @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {createSelector} from 'reselect'; @@ -10,6 +11,7 @@ import {getCurrentUserId, getUser, makeGetProfilesInChannel} from 'mattermost-re import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {goToScreen} from 'app/actions/navigation'; import {getChannelMembersForDm} from 'app/selectors/channel'; import ChannelIntro from './channel_intro'; @@ -52,4 +54,12 @@ function makeMapStateToProps() { }; } -export default connect(makeMapStateToProps)(ChannelIntro); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(makeMapStateToProps, mapDispatchToProps)(ChannelIntro); diff --git a/app/components/client_upgrade_listener/client_upgrade_listener.js b/app/components/client_upgrade_listener/client_upgrade_listener.js index 31631d8ca..b05b3f14f 100644 --- a/app/components/client_upgrade_listener/client_upgrade_listener.js +++ b/app/components/client_upgrade_listener/client_upgrade_listener.js @@ -27,6 +27,8 @@ export default class ClientUpgradeListener extends PureComponent { actions: PropTypes.shape({ logError: PropTypes.func.isRequired, setLastUpgradeCheck: PropTypes.func.isRequired, + showModal: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, }).isRequired, currentVersion: PropTypes.string, downloadLink: PropTypes.string, @@ -35,7 +37,6 @@ export default class ClientUpgradeListener extends PureComponent { lastUpgradeCheck: PropTypes.number, latestVersion: PropTypes.string, minVersion: PropTypes.string, - navigator: PropTypes.object, theme: PropTypes.object.isRequired, }; @@ -140,27 +141,26 @@ export default class ClientUpgradeListener extends PureComponent { }; handleLearnMore = () => { + const {actions} = this.props; const {intl} = this.context; - this.props.navigator.dismissModal({animationType: 'none'}); - this.props.navigator.showModal({ - screen: 'ClientUpgrade', - title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'}), - navigatorStyle: { - navBarHidden: false, - statusBarHidden: false, - statusBarHideWithNavBar: false, - }, - navigatorButtons: { + actions.dismissModal(); + + const screen = 'ClientUpgrade'; + const title = intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'}); + const passProps = { + upgradeType: this.state.upgradeType, + }; + const options = { + topBar: { leftButtons: [{ id: 'close-upgrade', icon: this.closeButton, }], }, - passProps: { - upgradeType: this.state.upgradeType, - }, - }); + }; + + actions.showModal(screen, title, passProps, options); this.toggleUpgradeMessage(false); }; diff --git a/app/components/client_upgrade_listener/index.js b/app/components/client_upgrade_listener/index.js index 2e66b2377..173c14a50 100644 --- a/app/components/client_upgrade_listener/index.js +++ b/app/components/client_upgrade_listener/index.js @@ -4,11 +4,12 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {logError} from 'mattermost-redux/actions/errors'; +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {showModal, dismissModal} from 'app/actions/navigation'; import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; import getClientUpgrade from 'app/selectors/client_upgrade'; import {isLandscape} from 'app/selectors/device'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import ClientUpgradeListener from './client_upgrade_listener'; @@ -32,6 +33,8 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ logError, setLastUpgradeCheck, + showModal, + dismissModal, }, dispatch), }; } diff --git a/app/components/combined_system_message/combined_system_message.js b/app/components/combined_system_message/combined_system_message.js index edf43bf40..72d6e82b0 100644 --- a/app/components/combined_system_message/combined_system_message.js +++ b/app/components/combined_system_message/combined_system_message.js @@ -177,7 +177,6 @@ export default class CombinedSystemMessage extends React.PureComponent { currentUserId: PropTypes.string.isRequired, currentUsername: PropTypes.string.isRequired, messageData: PropTypes.array.isRequired, - navigator: PropTypes.object.isRequired, showJoinLeave: PropTypes.bool.isRequired, textStyles: PropTypes.object, theme: PropTypes.object.isRequired, @@ -266,7 +265,6 @@ export default class CombinedSystemMessage extends React.PureComponent { const { currentUserId, currentUsername, - navigator, textStyles, theme, } = this.props; @@ -285,7 +283,6 @@ export default class CombinedSystemMessage extends React.PureComponent { diff --git a/app/components/combined_system_message/last_users.js b/app/components/combined_system_message/last_users.js index 5e026ad03..f85600ff7 100644 --- a/app/components/combined_system_message/last_users.js +++ b/app/components/combined_system_message/last_users.js @@ -53,7 +53,6 @@ export default class LastUsers extends React.PureComponent { static propTypes = { actor: PropTypes.string, expandedLocale: PropTypes.object.isRequired, - navigator: PropTypes.object.isRequired, postType: PropTypes.string.isRequired, style: PropTypes.object.isRequired, textStyles: PropTypes.object, @@ -88,7 +87,6 @@ export default class LastUsers extends React.PureComponent { const { actor, expandedLocale, - navigator, style, textStyles, usernames, @@ -106,7 +104,6 @@ export default class LastUsers extends React.PureComponent { return ( @@ -116,7 +113,6 @@ export default class LastUsers extends React.PureComponent { renderCollapsedView = () => { const { actor, - navigator, postType, style, textStyles, @@ -134,7 +130,6 @@ export default class LastUsers extends React.PureComponent { defaultMessage={'{firstUser} and '} values={{firstUser}} baseTextStyle={style.baseText} - navigator={navigator} style={style.baseText} textStyles={textStyles} theme={theme} @@ -155,7 +150,6 @@ export default class LastUsers extends React.PureComponent { defaultMessage={typeMessage[postType].defaultMessage} values={{actor}} baseTextStyle={style.baseText} - navigator={navigator} style={style.baseText} textStyles={textStyles} theme={theme} diff --git a/app/components/edit_channel_info/edit_channel_info.js b/app/components/edit_channel_info/edit_channel_info.js new file mode 100644 index 000000000..028240bdc --- /dev/null +++ b/app/components/edit_channel_info/edit_channel_info.js @@ -0,0 +1,415 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + Platform, + TouchableWithoutFeedback, + View, + Text, + 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 Loading from 'app/components/loading'; +import StatusBar from 'app/components/status_bar'; +import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder'; + +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import {General} from 'mattermost-redux/constants'; +import {getShortenedURL} from 'app/utils/url'; +import {t} from 'app/utils/i18n'; + +export default class EditChannelInfo extends PureComponent { + static propTypes = { + actions: PropTypes.shape({ + dismissModal: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, + }), + theme: PropTypes.object.isRequired, + deviceWidth: PropTypes.number.isRequired, + deviceHeight: PropTypes.number.isRequired, + channelType: PropTypes.string, + enableRightButton: PropTypes.func, + saving: PropTypes.bool.isRequired, + editing: PropTypes.bool, + error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + displayName: PropTypes.string, + currentTeamUrl: PropTypes.string, + channelURL: PropTypes.string, + purpose: PropTypes.string, + header: PropTypes.string, + onDisplayNameChange: PropTypes.func, + onChannelURLChange: PropTypes.func, + onPurposeChange: PropTypes.func, + onHeaderChange: PropTypes.func, + oldDisplayName: PropTypes.string, + oldChannelURL: PropTypes.string, + oldHeader: PropTypes.string, + oldPurpose: PropTypes.string, + }; + + static defaultProps = { + editing: false, + }; + + constructor(props) { + super(props); + + this.nameInput = React.createRef(); + this.urlInput = React.createRef(); + this.purposeInput = React.createRef(); + this.headerInput = React.createRef(); + this.lastText = React.createRef(); + this.scroll = React.createRef(); + } + + blur = () => { + if (this.nameInput?.current) { + this.nameInput.current.blur(); + } + + // TODO: uncomment below once the channel URL field is added + // if (this.urlInput?.current) { + // this.urlInput.current.blur(); + // } + + if (this.purposeInput?.current) { + this.purposeInput.current.blur(); + } + if (this.headerInput?.current) { + this.headerInput.current.blur(); + } + + if (this.scroll?.current) { + this.scroll.current.scrollToPosition(0, 0, true); + } + }; + + close = (goBack = false) => { + const {actions} = this.props; + if (goBack) { + actions.popTopScreen(); + } else { + actions.dismissModal(); + } + }; + + canUpdate = (displayName, channelURL, purpose, header) => { + const { + oldDisplayName, + oldChannelURL, + oldPurpose, + oldHeader, + } = this.props; + + return displayName !== oldDisplayName || channelURL !== oldChannelURL || + purpose !== oldPurpose || header !== oldHeader; + }; + + enableRightButton = (enable = false) => { + this.props.enableRightButton(enable); + }; + + onDisplayNameChangeText = (displayName) => { + const {editing, onDisplayNameChange} = this.props; + onDisplayNameChange(displayName); + + if (editing) { + const {channelURL, purpose, header} = this.props; + const canUpdate = this.canUpdate(displayName, channelURL, purpose, header); + this.enableRightButton(canUpdate); + return; + } + + const displayNameExists = displayName && displayName.length >= 2; + this.props.enableRightButton(displayNameExists); + }; + + onDisplayURLChangeText = (channelURL) => { + const {editing, onChannelURLChange} = this.props; + onChannelURLChange(channelURL); + + if (editing) { + const {displayName, purpose, header} = this.props; + const canUpdate = this.canUpdate(displayName, channelURL, purpose, header); + this.enableRightButton(canUpdate); + } + }; + + onPurposeChangeText = (purpose) => { + const {editing, onPurposeChange} = this.props; + onPurposeChange(purpose); + + if (editing) { + const {displayName, channelURL, header} = this.props; + const canUpdate = this.canUpdate(displayName, channelURL, purpose, header); + this.enableRightButton(canUpdate); + } + }; + + onHeaderChangeText = (header) => { + const {editing, onHeaderChange} = this.props; + onHeaderChange(header); + + if (editing) { + const {displayName, channelURL, purpose} = this.props; + const canUpdate = this.canUpdate(displayName, channelURL, purpose, header); + this.enableRightButton(canUpdate); + } + }; + + scrollToEnd = () => { + if (this.scroll?.current && this.lastText?.current) { + this.scroll.current.scrollToFocusedInput(findNodeHandle(this.lastText.current)); + } + }; + + render() { + const { + theme, + editing, + channelType, + currentTeamUrl, + deviceWidth, + deviceHeight, + displayName, + channelURL, + header, + purpose, + } = this.props; + const {error, saving} = this.props; + const fullUrl = currentTeamUrl + '/channels'; + const shortUrl = getShortenedURL(fullUrl, 35); + + const style = getStyleSheet(theme); + + const displayHeaderOnly = channelType === General.DM_CHANNEL || + channelType === General.GM_CHANNEL; + + if (saving) { + return ( + + + + + ); + } + + let displayError; + if (error) { + displayError = ( + + + + + + ); + } + + return ( + + + + {displayError} + + + {!displayHeaderOnly && ( + + + + + + + + + )} + {/*TODO: Hide channel url field until it's added to CreateChannel */} + {false && editing && !displayHeaderOnly && ( + + + + + {shortUrl} + + + + + + + )} + {!displayHeaderOnly && ( + + + + + + + + + + + + + )} + + + + + + + + + + + + + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + flex: 1, + backgroundColor: theme.centerChannelBg, + }, + scrollView: { + flex: 1, + backgroundColor: changeOpacity(theme.centerChannelColor, 0.03), + paddingTop: 10, + }, + errorContainer: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.03), + }, + errorWrapper: { + justifyContent: 'center', + alignItems: 'center', + }, + 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, + }, + }; +}); diff --git a/app/components/edit_channel_info/index.js b/app/components/edit_channel_info/index.js index 66927a3f1..a523704f5 100644 --- a/app/components/edit_channel_info/index.js +++ b/app/components/edit_channel_info/index.js @@ -1,414 +1,23 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + import { - Platform, - TouchableWithoutFeedback, - View, - Text, - findNodeHandle, -} from 'react-native'; -import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; + dismissModal, + popTopScreen, +} from 'app/actions/navigation'; -import ErrorText from 'app/components/error_text'; -import FormattedText from 'app/components/formatted_text'; -import Loading from 'app/components/loading'; -import StatusBar from 'app/components/status_bar'; -import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder'; +import EditChannelInfo from './edit_channel_info'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {General} from 'mattermost-redux/constants'; -import {getShortenedURL} from 'app/utils/url'; -import {t} from 'app/utils/i18n'; - -export default class EditChannelInfo extends PureComponent { - static propTypes = { - navigator: PropTypes.object.isRequired, - theme: PropTypes.object.isRequired, - deviceWidth: PropTypes.number.isRequired, - deviceHeight: PropTypes.number.isRequired, - channelType: PropTypes.string, - enableRightButton: PropTypes.func, - saving: PropTypes.bool.isRequired, - editing: PropTypes.bool, - error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - displayName: PropTypes.string, - currentTeamUrl: PropTypes.string, - channelURL: PropTypes.string, - purpose: PropTypes.string, - header: PropTypes.string, - onDisplayNameChange: PropTypes.func, - onChannelURLChange: PropTypes.func, - onPurposeChange: PropTypes.func, - onHeaderChange: PropTypes.func, - oldDisplayName: PropTypes.string, - oldChannelURL: PropTypes.string, - oldHeader: PropTypes.string, - oldPurpose: PropTypes.string, +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + dismissModal, + popTopScreen, + }, dispatch), }; - - static defaultProps = { - editing: false, - }; - - constructor(props) { - super(props); - - this.nameInput = React.createRef(); - this.urlInput = React.createRef(); - this.purposeInput = React.createRef(); - this.headerInput = React.createRef(); - this.lastText = React.createRef(); - this.scroll = React.createRef(); - } - - blur = () => { - if (this.nameInput?.current) { - this.nameInput.current.blur(); - } - - // TODO: uncomment below once the channel URL field is added - // if (this.urlInput?.current) { - // this.urlInput.current.blur(); - // } - - if (this.purposeInput?.current) { - this.purposeInput.current.blur(); - } - if (this.headerInput?.current) { - this.headerInput.current.blur(); - } - - if (this.scroll?.current) { - this.scroll.current.scrollToPosition(0, 0, true); - } - }; - - close = (goBack = false) => { - if (goBack) { - this.props.navigator.pop({animated: true}); - } else { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); - } - }; - - canUpdate = (displayName, channelURL, purpose, header) => { - const { - oldDisplayName, - oldChannelURL, - oldPurpose, - oldHeader, - } = this.props; - - return displayName !== oldDisplayName || channelURL !== oldChannelURL || - purpose !== oldPurpose || header !== oldHeader; - }; - - enableRightButton = (enable = false) => { - this.props.enableRightButton(enable); - }; - - onDisplayNameChangeText = (displayName) => { - const {editing, onDisplayNameChange} = this.props; - onDisplayNameChange(displayName); - - if (editing) { - const {channelURL, purpose, header} = this.props; - const canUpdate = this.canUpdate(displayName, channelURL, purpose, header); - this.enableRightButton(canUpdate); - return; - } - - const displayNameExists = displayName && displayName.length >= 2; - this.props.enableRightButton(displayNameExists); - }; - - onDisplayURLChangeText = (channelURL) => { - const {editing, onChannelURLChange} = this.props; - onChannelURLChange(channelURL); - - if (editing) { - const {displayName, purpose, header} = this.props; - const canUpdate = this.canUpdate(displayName, channelURL, purpose, header); - this.enableRightButton(canUpdate); - } - }; - - onPurposeChangeText = (purpose) => { - const {editing, onPurposeChange} = this.props; - onPurposeChange(purpose); - - if (editing) { - const {displayName, channelURL, header} = this.props; - const canUpdate = this.canUpdate(displayName, channelURL, purpose, header); - this.enableRightButton(canUpdate); - } - }; - - onHeaderChangeText = (header) => { - const {editing, onHeaderChange} = this.props; - onHeaderChange(header); - - if (editing) { - const {displayName, channelURL, purpose} = this.props; - const canUpdate = this.canUpdate(displayName, channelURL, purpose, header); - this.enableRightButton(canUpdate); - } - }; - - scrollToEnd = () => { - if (this.scroll?.current && this.lastText?.current) { - this.scroll.current.scrollToFocusedInput(findNodeHandle(this.lastText.current)); - } - }; - - render() { - const { - theme, - editing, - channelType, - currentTeamUrl, - deviceWidth, - deviceHeight, - displayName, - channelURL, - header, - purpose, - } = this.props; - const {error, saving} = this.props; - const fullUrl = currentTeamUrl + '/channels'; - const shortUrl = getShortenedURL(fullUrl, 35); - - const style = getStyleSheet(theme); - - const displayHeaderOnly = channelType === General.DM_CHANNEL || - channelType === General.GM_CHANNEL; - - if (saving) { - return ( - - - - - ); - } - - let displayError; - if (error) { - displayError = ( - - - - - - ); - } - - return ( - - - - {displayError} - - - {!displayHeaderOnly && ( - - - - - - - - - )} - {/*TODO: Hide channel url field until it's added to CreateChannel */} - {false && editing && !displayHeaderOnly && ( - - - - - {shortUrl} - - - - - - - )} - {!displayHeaderOnly && ( - - - - - - - - - - - - - )} - - - - - - - - - - - - - - - ); - } } -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - flex: 1, - backgroundColor: theme.centerChannelBg, - }, - scrollView: { - flex: 1, - backgroundColor: changeOpacity(theme.centerChannelColor, 0.03), - paddingTop: 10, - }, - errorContainer: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.03), - }, - errorWrapper: { - justifyContent: 'center', - alignItems: 'center', - }, - 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 connect(null, mapDispatchToProps)(EditChannelInfo); diff --git a/app/components/file_attachment_list/file_attachment.js b/app/components/file_attachment_list/file_attachment.js index f190cf0ee..6efe9563c 100644 --- a/app/components/file_attachment_list/file_attachment.js +++ b/app/components/file_attachment_list/file_attachment.js @@ -29,7 +29,6 @@ export default class FileAttachment extends PureComponent { onLongPress: PropTypes.func, onPreviewPress: PropTypes.func, theme: PropTypes.object.isRequired, - navigator: PropTypes.object, }; static defaultProps = { @@ -93,7 +92,6 @@ export default class FileAttachment extends PureComponent { deviceWidth, file, theme, - navigator, onLongPress, } = this.props; const {data} = file; @@ -120,7 +118,6 @@ export default class FileAttachment extends PureComponent { ref={this.setDocumentRef} canDownloadFiles={canDownloadFiles} file={file} - navigator={navigator} onLongPress={onLongPress} theme={theme} /> diff --git a/app/components/file_attachment_list/file_attachment_document.js b/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js similarity index 94% rename from app/components/file_attachment_list/file_attachment_document.js rename to app/components/file_attachment_list/file_attachment_document/file_attachment_document.js index 473463227..d699b9a29 100644 --- a/app/components/file_attachment_list/file_attachment_document.js +++ b/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js @@ -25,7 +25,7 @@ import {DeviceTypes} from 'app/constants/'; import mattermostBucket from 'app/mattermost_bucket'; import {changeOpacity} from 'app/utils/theme'; -import FileAttachmentIcon from './file_attachment_icon'; +import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon'; const {DOCUMENTS_PATH} = DeviceTypes; const DOWNLOADING_OFFSET = 28; @@ -38,13 +38,15 @@ const circularProgressWidth = 4; export default class FileAttachmentDocument extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, backgroundColor: PropTypes.string, canDownloadFiles: PropTypes.bool.isRequired, iconHeight: PropTypes.number, iconWidth: PropTypes.number, file: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, - navigator: PropTypes.object, onLongPress: PropTypes.func, wrapperHeight: PropTypes.number, wrapperWidth: PropTypes.number, @@ -192,7 +194,7 @@ export default class FileAttachmentDocument extends PureComponent { }; previewTextFile = (file, delay = 2000) => { - const {navigator, theme} = this.props; + const {actions} = this.props; const {data} = file; const prefix = Platform.OS === 'android' ? 'file:/' : ''; const path = `${DOCUMENTS_PATH}/${data.id}-${file.caption}`; @@ -200,21 +202,13 @@ export default class FileAttachmentDocument extends PureComponent { setTimeout(async () => { try { const content = await readFile; - navigator.push({ - screen: 'TextPreview', - title: file.caption, - animated: true, - backButtonTitle: '', - passProps: { - content, - }, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }); + const screen = 'TextPreview'; + const title = file.caption; + const passProps = { + content, + }; + + actions.goToScreen(screen, title, passProps); this.setState({downloading: false, progress: 0}); } catch (error) { RNFetchBlob.fs.unlink(path); diff --git a/app/components/file_attachment_list/file_attachment_document/index.js b/app/components/file_attachment_list/file_attachment_document/index.js new file mode 100644 index 000000000..753d2d8ff --- /dev/null +++ b/app/components/file_attachment_list/file_attachment_document/index.js @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {goToScreen} from 'app/actions/navigation'; + +import FileAttachmentDocument from './file_attachment_document'; + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(null, mapDispatchToProps)(FileAttachmentDocument); diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js index f53eef22c..80f7d463c 100644 --- a/app/components/file_attachment_list/file_attachment_list.js +++ b/app/components/file_attachment_list/file_attachment_list.js @@ -20,14 +20,16 @@ import FileAttachment from './file_attachment'; export default class FileAttachmentList extends Component { static propTypes = { - actions: PropTypes.object.isRequired, + actions: PropTypes.shape({ + loadFilesForPostIfNecessary: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, + }).isRequired, canDownloadFiles: PropTypes.bool.isRequired, deviceHeight: PropTypes.number.isRequired, deviceWidth: PropTypes.number.isRequired, fileIds: PropTypes.array.isRequired, files: PropTypes.array, isFailed: PropTypes.bool, - navigator: PropTypes.object, onLongPress: PropTypes.func, postId: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, @@ -122,11 +124,12 @@ export default class FileAttachmentList extends Component { }; handlePreviewPress = preventDoubleTap((idx) => { - previewImageAtIndex(this.props.navigator, this.items, idx, this.galleryFiles); + const {actions} = this.props; + previewImageAtIndex(this.items, idx, this.galleryFiles, actions.showModalOverCurrentContext); }); renderItems = () => { - const {canDownloadFiles, deviceWidth, fileIds, files, navigator} = this.props; + const {canDownloadFiles, deviceWidth, fileIds, files} = this.props; if (!files.length && fileIds.length > 0) { return fileIds.map((id, idx) => ( @@ -156,7 +159,6 @@ export default class FileAttachmentList extends Component { file={f} id={file.id} index={idx} - navigator={navigator} onCaptureRef={this.handleCaptureRef} onPreviewPress={this.handlePreviewPress} onLongPress={this.props.onLongPress} diff --git a/app/components/file_attachment_list/file_attachment_list.test.js b/app/components/file_attachment_list/file_attachment_list.test.js index 245e0bac1..e2035d542 100644 --- a/app/components/file_attachment_list/file_attachment_list.test.js +++ b/app/components/file_attachment_list/file_attachment_list.test.js @@ -16,6 +16,7 @@ describe('PostAttachmentOpenGraph', () => { const baseProps = { actions: { loadFilesForPostIfNecessary, + showModalOverCurrentContext: jest.fn(), }, canDownloadFiles: true, deviceHeight: 680, @@ -71,6 +72,7 @@ describe('PostAttachmentOpenGraph', () => { files: [], actions: { loadFilesForPostIfNecessary: loadFilesForPostIfNecessaryMock, + showModalOverCurrentContext: jest.fn(), }, }; diff --git a/app/components/file_attachment_list/index.js b/app/components/file_attachment_list/index.js index 2230cd5df..29960f63a 100644 --- a/app/components/file_attachment_list/index.js +++ b/app/components/file_attachment_list/index.js @@ -8,6 +8,7 @@ import {canDownloadFilesOnMobile} from 'mattermost-redux/selectors/entities/gene import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {showModalOverCurrentContext} from 'app/actions/navigation'; import {loadFilesForPostIfNecessary} from 'app/actions/views/channel'; import {getDimensions} from 'app/selectors/device'; @@ -29,6 +30,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ loadFilesForPostIfNecessary, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/components/formatted_markdown_text.js b/app/components/formatted_markdown_text.js index 965280d45..2f5b03fc2 100644 --- a/app/components/formatted_markdown_text.js +++ b/app/components/formatted_markdown_text.js @@ -35,7 +35,6 @@ class FormattedMarkdownText extends React.PureComponent { baseTextStyle: CustomPropTypes.Style, defaultMessage: PropTypes.string.isRequired, id: PropTypes.string.isRequired, - navigator: PropTypes.object.isRequired, onPostPress: PropTypes.func, style: CustomPropTypes.Style, textStyles: PropTypes.object, @@ -115,7 +114,6 @@ class FormattedMarkdownText extends React.PureComponent { diff --git a/app/components/interactive_dialog_controller/index.js b/app/components/interactive_dialog_controller/index.js index 10d93d094..a2577835a 100644 --- a/app/components/interactive_dialog_controller/index.js +++ b/app/components/interactive_dialog_controller/index.js @@ -1,8 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; +import {showModal} from 'app/actions/navigation'; + import InteractiveDialogController from './interactive_dialog_controller'; function mapStateToProps(state) { @@ -12,4 +15,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(InteractiveDialogController); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + showModal, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(InteractiveDialogController); diff --git a/app/components/interactive_dialog_controller/interactive_dialog_controller.js b/app/components/interactive_dialog_controller/interactive_dialog_controller.js index fcba9b347..7ec4e0177 100644 --- a/app/components/interactive_dialog_controller/interactive_dialog_controller.js +++ b/app/components/interactive_dialog_controller/interactive_dialog_controller.js @@ -7,9 +7,11 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; export default class InteractiveDialogController extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + showModal: PropTypes.func.isRequired, + }).isRequired, triggerId: PropTypes.string, dialog: PropTypes.object, - navigator: PropTypes.object, theme: PropTypes.object, }; @@ -22,7 +24,7 @@ export default class InteractiveDialogController extends PureComponent { } componentDidUpdate(prevProps) { - const triggerId = this.props.triggerId; + const {actions, triggerId} = this.props; if (!triggerId) { return; } @@ -41,33 +43,24 @@ export default class InteractiveDialogController extends PureComponent { return; } - const theme = this.props.theme; - - this.props.navigator.showModal({ - backButtonTitle: '', - screen: 'InteractiveDialog', - title: dialogData.dialog.title, - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - navigatorButtons: { + const screen = 'InteractiveDialog'; + const title = dialogData.dialog.title; + const passProps = {}; + const options = { + topBar: { leftButtons: [{ id: 'close-dialog', icon: this.closeButton, }], - rightButtons: [ - { - id: 'submit-dialog', - showAsAction: 'always', - title: dialogData.dialog.submit_label, - }, - ], + rightButtons: [{ + id: 'submit-dialog', + showAsAction: 'always', + text: dialogData.dialog.submit_label, + }], }, - }); + }; + + actions.showModal(screen, title, passProps, options); } render() { diff --git a/app/components/markdown/hashtag/hashtag.js b/app/components/markdown/hashtag/hashtag.js index 75fc5486f..540d49bff 100644 --- a/app/components/markdown/hashtag/hashtag.js +++ b/app/components/markdown/hashtag/hashtag.js @@ -12,24 +12,31 @@ export default class Hashtag extends React.PureComponent { hashtag: PropTypes.string.isRequired, linkStyle: CustomPropTypes.Style.isRequired, onHashtagPress: PropTypes.func, - navigator: PropTypes.object.isRequired, actions: PropTypes.shape({ + popToRoot: PropTypes.func.isRequired, showSearchModal: PropTypes.func.isRequired, + dismissAllModals: PropTypes.func.isRequired, }).isRequired, }; handlePress = () => { - if (this.props.onHashtagPress) { - this.props.onHashtagPress(this.props.hashtag); + const { + onHashtagPress, + hashtag, + actions, + } = this.props; + + if (onHashtagPress) { + onHashtagPress(hashtag); return; } // Close thread view, permalink view, etc - this.props.navigator.dismissAllModals(); - this.props.navigator.popToRoot(); + actions.dismissAllModals(); + actions.popToRoot(); - this.props.actions.showSearchModal(this.props.navigator, '#' + this.props.hashtag); + actions.showSearchModal('#' + this.props.hashtag); }; render() { diff --git a/app/components/markdown/hashtag/hashtag.test.js b/app/components/markdown/hashtag/hashtag.test.js index 05d87e3dd..d75fc2a74 100644 --- a/app/components/markdown/hashtag/hashtag.test.js +++ b/app/components/markdown/hashtag/hashtag.test.js @@ -11,12 +11,10 @@ describe('Hashtag', () => { const baseProps = { hashtag: 'test', linkStyle: {color: 'red'}, - navigator: { - dismissAllModals: jest.fn(), - popToRoot: jest.fn(), - }, actions: { showSearchModal: jest.fn(), + dismissAllModals: jest.fn(), + popToRoot: jest.fn(), }, }; @@ -35,9 +33,9 @@ describe('Hashtag', () => { wrapper.find(Text).simulate('press'); - expect(props.navigator.dismissAllModals).toHaveBeenCalled(); - expect(props.navigator.popToRoot).toHaveBeenCalled(); - expect(props.actions.showSearchModal).toHaveBeenCalledWith(props.navigator, '#test'); + expect(props.actions.dismissAllModals).toHaveBeenCalled(); + expect(props.actions.popToRoot).toHaveBeenCalled(); + expect(props.actions.showSearchModal).toHaveBeenCalledWith('#test'); }); test('should call onHashtagPress if provided', () => { @@ -50,8 +48,8 @@ describe('Hashtag', () => { wrapper.find(Text).simulate('press'); - expect(props.navigator.dismissAllModals).not.toBeCalled(); - expect(props.navigator.popToRoot).not.toBeCalled(); + expect(props.actions.dismissAllModals).not.toBeCalled(); + expect(props.actions.popToRoot).not.toBeCalled(); expect(props.actions.showSearchModal).not.toBeCalled(); expect(props.onHashtagPress).toBeCalled(); diff --git a/app/components/markdown/hashtag/index.js b/app/components/markdown/hashtag/index.js index 3683f71ab..9ffc10e5c 100644 --- a/app/components/markdown/hashtag/index.js +++ b/app/components/markdown/hashtag/index.js @@ -4,14 +4,20 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {showSearchModal} from 'app/actions/views/search'; +import { + popToRoot, + showSearchModal, + dismissAllModals, +} from 'app/actions/navigation'; import Hashtag from './hashtag'; function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ + popToRoot, showSearchModal, + dismissAllModals, }, dispatch), }; } diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js index 80aa2df39..93edb1296 100644 --- a/app/components/markdown/markdown.js +++ b/app/components/markdown/markdown.js @@ -49,7 +49,6 @@ export default class Markdown extends PureComponent { isSearchResult: PropTypes.bool, mentionKeys: PropTypes.array.isRequired, minimumHashtagLength: PropTypes.number.isRequired, - navigator: PropTypes.object.isRequired, onChannelLinkPress: PropTypes.func, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, @@ -176,7 +175,6 @@ export default class Markdown extends PureComponent { {reactChildren} @@ -188,7 +186,6 @@ export default class Markdown extends PureComponent { linkDestination={linkDestination} imagesMetadata={this.props.imagesMetadata} isReplyPost={this.props.isReplyPost} - navigator={this.props.navigator} source={src} errorTextStyle={[this.computeTextStyle(this.props.baseTextStyle, context), this.props.textStyles.error]} > @@ -209,7 +206,6 @@ export default class Markdown extends PureComponent { isSearchResult={this.props.isSearchResult} mentionName={mentionName} onPostPress={this.props.onPostPress} - navigator={this.props.navigator} /> ); }; @@ -250,7 +246,6 @@ export default class Markdown extends PureComponent { hashtag={hashtag} linkStyle={this.props.textStyles.link} onHashtagPress={this.props.onHashtagPress} - navigator={this.props.navigator} /> ); }; @@ -295,7 +290,6 @@ export default class Markdown extends PureComponent { return ( { return ( {children} diff --git a/app/components/markdown/markdown_code_block/index.js b/app/components/markdown/markdown_code_block/index.js index 37ccabce4..76e8cbb51 100644 --- a/app/components/markdown/markdown_code_block/index.js +++ b/app/components/markdown/markdown_code_block/index.js @@ -1,10 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {goToScreen} from 'app/actions/navigation'; + import MarkdownCodeBlock from './markdown_code_block'; function mapStateToProps(state) { @@ -13,4 +16,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(MarkdownCodeBlock); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(MarkdownCodeBlock); diff --git a/app/components/markdown/markdown_code_block/markdown_code_block.js b/app/components/markdown/markdown_code_block/markdown_code_block.js index d6ff764ed..eaef682e2 100644 --- a/app/components/markdown/markdown_code_block/markdown_code_block.js +++ b/app/components/markdown/markdown_code_block/markdown_code_block.js @@ -24,7 +24,9 @@ const MAX_LINES = 4; export default class MarkdownCodeBlock extends React.PureComponent { static propTypes = { - navigator: PropTypes.object.isRequired, + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, theme: PropTypes.object.isRequired, language: PropTypes.string, content: PropTypes.string.isRequired, @@ -40,10 +42,14 @@ export default class MarkdownCodeBlock extends React.PureComponent { }; handlePress = preventDoubleTap(() => { - const {navigator, theme} = this.props; + const {actions, language, content} = this.props; const {intl} = this.context; + const screen = 'Code'; + const passProps = { + content, + }; - const languageDisplayName = getDisplayNameForLanguage(this.props.language); + const languageDisplayName = getDisplayNameForLanguage(language); let title; if (languageDisplayName) { title = intl.formatMessage( @@ -62,21 +68,7 @@ export default class MarkdownCodeBlock extends React.PureComponent { }); } - navigator.push({ - screen: 'Code', - title, - animated: true, - backButtonTitle: '', - passProps: { - content: this.props.content, - }, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }); + actions.goToScreen(screen, title, passProps); }); handleLongPress = async () => { diff --git a/app/components/markdown/markdown_image/index.js b/app/components/markdown/markdown_image/index.js index 19ef387fa..f15c54ccc 100644 --- a/app/components/markdown/markdown_image/index.js +++ b/app/components/markdown/markdown_image/index.js @@ -1,10 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; +import {showModalOverCurrentContext} from 'app/actions/navigation'; + import {getDimensions} from 'app/selectors/device'; import MarkdownImage from './markdown_image'; @@ -16,4 +19,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(MarkdownImage); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + showModalOverCurrentContext, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(MarkdownImage); diff --git a/app/components/markdown/markdown_image/markdown_image.js b/app/components/markdown/markdown_image/markdown_image.js index 6f9fc9bf5..526bb4a82 100644 --- a/app/components/markdown/markdown_image/markdown_image.js +++ b/app/components/markdown/markdown_image/markdown_image.js @@ -33,13 +33,15 @@ const VIEWPORT_IMAGE_REPLY_OFFSET = 13; export default class MarkdownImage extends React.Component { static propTypes = { + actions: PropTypes.shape({ + showModalOverCurrentContext: PropTypes.func.isRequired, + }).isRequired, children: PropTypes.node, deviceHeight: PropTypes.number.isRequired, deviceWidth: PropTypes.number.isRequired, imagesMetadata: PropTypes.object, linkDestination: PropTypes.string, isReplyPost: PropTypes.bool, - navigator: PropTypes.object.isRequired, serverURL: PropTypes.string.isRequired, source: PropTypes.string.isRequired, errorTextStyle: CustomPropTypes.Style, @@ -175,6 +177,7 @@ export default class MarkdownImage extends React.Component { originalWidth, uri, } = this.state; + const {actions} = this.props; const link = this.getSource(); let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?')); const extension = filename.split('.').pop(); @@ -195,7 +198,8 @@ export default class MarkdownImage extends React.Component { localPath: uri, }, }]; - previewImageAtIndex(this.props.navigator, [this.refs.item], 0, files); + + previewImageAtIndex([this.refs.item], 0, files, actions.showModalOverCurrentContext); }; loadImageSize = (source) => { diff --git a/app/components/markdown/markdown_table/index.js b/app/components/markdown/markdown_table/index.js index e3e37c016..4100a3fc4 100644 --- a/app/components/markdown/markdown_table/index.js +++ b/app/components/markdown/markdown_table/index.js @@ -1,10 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {goToScreen} from 'app/actions/navigation'; + import MarkdownTable from './markdown_table'; function mapStateToProps(state) { @@ -13,4 +16,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(MarkdownTable); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(MarkdownTable); diff --git a/app/components/markdown/markdown_table/markdown_table.js b/app/components/markdown/markdown_table/markdown_table.js index 2d6cf6d73..deffa3174 100644 --- a/app/components/markdown/markdown_table/markdown_table.js +++ b/app/components/markdown/markdown_table/markdown_table.js @@ -19,8 +19,10 @@ const MAX_HEIGHT = 300; export default class MarkdownTable extends React.PureComponent { static propTypes = { + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, children: PropTypes.node.isRequired, - navigator: PropTypes.object.isRequired, numColumns: PropTypes.number.isRequired, theme: PropTypes.object.isRequired, }; @@ -44,27 +46,19 @@ export default class MarkdownTable extends React.PureComponent { }; handlePress = preventDoubleTap(() => { - const {navigator, theme} = this.props; - - navigator.push({ - screen: 'Table', - title: this.context.intl.formatMessage({ - id: 'mobile.routes.table', - defaultMessage: 'Table', - }), - animated: true, - backButtonTitle: '', - passProps: { - renderRows: this.renderRows, - tableWidth: this.getTableWidth(), - }, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, + const {actions} = this.props; + const {intl} = this.context; + const screen = 'Table'; + const title = intl.formatMessage({ + id: 'mobile.routes.table', + defaultMessage: 'Table', }); + const passProps = { + renderRows: this.renderRows, + tableWidth: this.getTableWidth(), + }; + + actions.goToScreen(screen, title, passProps); }); handleContainerLayout = (e) => { diff --git a/app/components/markdown/markdown_table_image/index.js b/app/components/markdown/markdown_table_image/index.js index 96174a3a8..c1eea7804 100644 --- a/app/components/markdown/markdown_table_image/index.js +++ b/app/components/markdown/markdown_table_image/index.js @@ -1,11 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {goToScreen} from 'app/actions/navigation'; + import MarkdownTableImage from './markdown_table_image'; function mapStateToProps(state) { @@ -15,4 +18,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(MarkdownTableImage); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(MarkdownTableImage); diff --git a/app/components/markdown/markdown_table_image/markdown_table_image.js b/app/components/markdown/markdown_table_image/markdown_table_image.js index c75942201..b2fdfd5f3 100644 --- a/app/components/markdown/markdown_table_image/markdown_table_image.js +++ b/app/components/markdown/markdown_table_image/markdown_table_image.js @@ -11,10 +11,12 @@ import {preventDoubleTap} from 'app/utils/tap'; export default class MarkdownTableImage extends React.PureComponent { static propTypes = { + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, children: PropTypes.node.isRequired, source: PropTypes.string.isRequired, textStyle: CustomPropTypes.Style.isRequired, - navigator: PropTypes.object.isRequired, serverURL: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, }; @@ -24,26 +26,18 @@ export default class MarkdownTableImage extends React.PureComponent { }; handlePress = preventDoubleTap(() => { - const {navigator, theme} = this.props; - - navigator.push({ - screen: 'TableImage', - title: this.context.intl.formatMessage({ - id: 'mobile.routes.tableImage', - defaultMessage: 'Image', - }), - animated: true, - backButtonTitle: '', - passProps: { - imageSource: this.getImageSource(), - }, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, + const {actions} = this.props; + const {intl} = this.context; + const screen = 'TableImage'; + const title = intl.formatMessage({ + id: 'mobile.routes.tableImage', + defaultMessage: 'Image', }); + const passProps = { + imageSource: this.getImageSource(), + }; + + actions.goToScreen(screen, title, passProps); }); getImageSource = () => { diff --git a/app/components/message_attachments/action_menu/action_menu.js b/app/components/message_attachments/action_menu/action_menu.js index 96197ecf0..3ea8d46fd 100644 --- a/app/components/message_attachments/action_menu/action_menu.js +++ b/app/components/message_attachments/action_menu/action_menu.js @@ -18,7 +18,6 @@ export default class ActionMenu extends PureComponent { options: PropTypes.arrayOf(PropTypes.object), postId: PropTypes.string.isRequired, selected: PropTypes.object, - navigator: PropTypes.object, }; constructor(props) { @@ -63,7 +62,6 @@ export default class ActionMenu extends PureComponent { name, dataSource, options, - navigator, } = this.props; const {selected} = this.state; @@ -73,7 +71,6 @@ export default class ActionMenu extends PureComponent { dataSource={dataSource} options={options} selected={selected} - navigator={navigator} onSelected={this.handleSelect} /> ); diff --git a/app/components/message_attachments/attachment_actions.js b/app/components/message_attachments/attachment_actions.js index 865d1fb66..9f4203fbd 100644 --- a/app/components/message_attachments/attachment_actions.js +++ b/app/components/message_attachments/attachment_actions.js @@ -10,14 +10,12 @@ import ActionButton from './action_button'; export default class AttachmentActions extends PureComponent { static propTypes = { actions: PropTypes.array, - navigator: PropTypes.object.isRequired, postId: PropTypes.string.isRequired, }; render() { const { actions, - navigator, postId, } = this.props; @@ -43,7 +41,6 @@ export default class AttachmentActions extends PureComponent { defaultOption={action.default_option} options={action.options} postId={postId} - navigator={navigator} /> ); break; diff --git a/app/components/message_attachments/attachment_fields.js b/app/components/message_attachments/attachment_fields.js index 67d76dda4..ba96553d0 100644 --- a/app/components/message_attachments/attachment_fields.js +++ b/app/components/message_attachments/attachment_fields.js @@ -15,7 +15,6 @@ export default class AttachmentFields extends PureComponent { blockStyles: PropTypes.object.isRequired, fields: PropTypes.array, metadata: PropTypes.object, - navigator: PropTypes.object.isRequired, onPermalinkPress: PropTypes.func, textStyles: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, @@ -27,7 +26,6 @@ export default class AttachmentFields extends PureComponent { blockStyles, fields, metadata, - navigator, onPermalinkPress, textStyles, theme, @@ -88,7 +86,6 @@ export default class AttachmentFields extends PureComponent { blockStyles={blockStyles} imagesMetadata={metadata?.images} value={(field.value || '')} - navigator={navigator} onPermalinkPress={onPermalinkPress} /> diff --git a/app/components/message_attachments/attachment_image.js b/app/components/message_attachments/attachment_image/attachment_image.js similarity index 95% rename from app/components/message_attachments/attachment_image.js rename to app/components/message_attachments/attachment_image/attachment_image.js index 249c1e9ec..dfb04ac13 100644 --- a/app/components/message_attachments/attachment_image.js +++ b/app/components/message_attachments/attachment_image/attachment_image.js @@ -15,11 +15,13 @@ const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10; export default class AttachmentImage extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + showModalOverCurrentContext: PropTypes.func.isRequired, + }).isRequired, deviceHeight: PropTypes.number.isRequired, deviceWidth: PropTypes.number.isRequired, imageMetadata: PropTypes.object, imageUrl: PropTypes.string, - navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, }; @@ -47,7 +49,7 @@ export default class AttachmentImage extends PureComponent { } handlePreviewImage = () => { - const {imageUrl, navigator} = this.props; + const {actions, imageUrl} = this.props; const { imageUri: uri, originalHeight, @@ -73,7 +75,7 @@ export default class AttachmentImage extends PureComponent { localPath: uri, }, }]; - previewImageAtIndex(navigator, [this.refs.item], 0, files); + previewImageAtIndex([this.refs.item], 0, files, actions.showModalOverCurrentContext); }; setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => { diff --git a/app/components/message_attachments/attachment_image/index.js b/app/components/message_attachments/attachment_image/index.js new file mode 100644 index 000000000..17bfedae3 --- /dev/null +++ b/app/components/message_attachments/attachment_image/index.js @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {showModalOverCurrentContext} from 'app/actions/navigation'; + +import AttachmentImage from './attachment_image'; + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + showModalOverCurrentContext, + }, dispatch), + }; +} + +export default connect(null, mapDispatchToProps)(AttachmentImage); diff --git a/app/components/message_attachments/attachment_pretext.js b/app/components/message_attachments/attachment_pretext.js index fcb1e900d..827af5ca7 100644 --- a/app/components/message_attachments/attachment_pretext.js +++ b/app/components/message_attachments/attachment_pretext.js @@ -13,7 +13,6 @@ export default class AttachmentPreText extends PureComponent { baseTextStyle: CustomPropTypes.Style.isRequired, blockStyles: PropTypes.object.isRequired, metadata: PropTypes.object, - navigator: PropTypes.object.isRequired, onPermalinkPress: PropTypes.func, textStyles: PropTypes.object.isRequired, value: PropTypes.string, @@ -24,7 +23,6 @@ export default class AttachmentPreText extends PureComponent { baseTextStyle, blockStyles, metadata, - navigator, onPermalinkPress, value, textStyles, @@ -42,7 +40,6 @@ export default class AttachmentPreText extends PureComponent { blockStyles={blockStyles} imagesMetadata={metadata?.images} value={value} - navigator={navigator} onPermalinkPress={onPermalinkPress} /> diff --git a/app/components/message_attachments/attachment_text.js b/app/components/message_attachments/attachment_text.js index 66ef572ca..b155d5178 100644 --- a/app/components/message_attachments/attachment_text.js +++ b/app/components/message_attachments/attachment_text.js @@ -18,7 +18,6 @@ export default class AttachmentText extends PureComponent { deviceHeight: PropTypes.number.isRequired, hasThumbnail: PropTypes.bool, metadata: PropTypes.object, - navigator: PropTypes.object.isRequired, onPermalinkPress: PropTypes.func, textStyles: PropTypes.object.isRequired, value: PropTypes.string, @@ -68,7 +67,6 @@ export default class AttachmentText extends PureComponent { blockStyles, hasThumbnail, metadata, - navigator, onPermalinkPress, value, textStyles, @@ -97,7 +95,6 @@ export default class AttachmentText extends PureComponent { blockStyles={blockStyles} imagesMetadata={metadata?.images} value={value} - navigator={navigator} onPermalinkPress={onPermalinkPress} /> diff --git a/app/components/message_attachments/attachment_title.js b/app/components/message_attachments/attachment_title.js index bfd673edd..12a525748 100644 --- a/app/components/message_attachments/attachment_title.js +++ b/app/components/message_attachments/attachment_title.js @@ -13,7 +13,6 @@ export default class AttachmentTitle extends PureComponent { link: PropTypes.string, theme: PropTypes.object.isRequired, value: PropTypes.string, - navigator: PropTypes.object.isRequired, }; openLink = () => { @@ -28,7 +27,6 @@ export default class AttachmentTitle extends PureComponent { link, value, theme, - navigator, } = this.props; if (!value) { @@ -57,7 +55,6 @@ export default class AttachmentTitle extends PureComponent { disableChannelLink={true} autolinkedUrlSchemes={[]} mentionKeys={[]} - navigator={navigator} theme={theme} value={value} baseTextStyle={style.title} diff --git a/app/components/message_attachments/index.js b/app/components/message_attachments/index.js index e3b287545..7f3c126ce 100644 --- a/app/components/message_attachments/index.js +++ b/app/components/message_attachments/index.js @@ -18,7 +18,6 @@ export default class MessageAttachments extends PureComponent { deviceWidth: PropTypes.number.isRequired, postId: PropTypes.string.isRequired, metadata: PropTypes.object, - navigator: PropTypes.object.isRequired, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, theme: PropTypes.object, @@ -33,7 +32,6 @@ export default class MessageAttachments extends PureComponent { deviceHeight, deviceWidth, metadata, - navigator, onHashtagPress, onPermalinkPress, postId, @@ -52,7 +50,6 @@ export default class MessageAttachments extends PureComponent { deviceWidth={deviceWidth} key={'att_' + i} metadata={metadata} - navigator={navigator} onHashtagPress={onHashtagPress} onPermalinkPress={onPermalinkPress} postId={postId} diff --git a/app/components/message_attachments/message_attachment.js b/app/components/message_attachments/message_attachment.js index eafba47aa..5e544b6c8 100644 --- a/app/components/message_attachments/message_attachment.js +++ b/app/components/message_attachments/message_attachment.js @@ -31,7 +31,6 @@ export default class MessageAttachment extends PureComponent { deviceHeight: PropTypes.number.isRequired, deviceWidth: PropTypes.number.isRequired, metadata: PropTypes.object, - navigator: PropTypes.object.isRequired, postId: PropTypes.string.isRequired, onPermalinkPress: PropTypes.func, theme: PropTypes.object, @@ -46,7 +45,6 @@ export default class MessageAttachment extends PureComponent { deviceHeight, deviceWidth, metadata, - navigator, onPermalinkPress, postId, textStyles, @@ -70,7 +68,6 @@ export default class MessageAttachment extends PureComponent { baseTextStyle={baseTextStyle} blockStyles={blockStyles} metadata={metadata} - navigator={navigator} onPermalinkPress={onPermalinkPress} textStyles={textStyles} value={attachment.pretext} @@ -86,7 +83,6 @@ export default class MessageAttachment extends PureComponent { link={attachment.title_link} theme={theme} value={attachment.title} - navigator={navigator} /> diff --git a/app/components/post/index.js b/app/components/post/index.js index bd501d2ff..300c7feaf 100644 --- a/app/components/post/index.js +++ b/app/components/post/index.js @@ -12,6 +12,7 @@ import {getUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/use import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils'; +import {goToScreen, showModalOverCurrentContext} from 'app/actions/navigation'; import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel'; import Post from './post'; @@ -93,6 +94,8 @@ function mapDispatchToProps(dispatch) { removePost, setPostTooltipVisible, insertToDraft, + goToScreen, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/components/post/post.js b/app/components/post/post.js index 0fc4e6f85..4345e0955 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -34,6 +34,8 @@ export default class Post extends PureComponent { createPost: PropTypes.func.isRequired, insertToDraft: PropTypes.func.isRequired, removePost: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }).isRequired, channelIsReadOnly: PropTypes.bool, currentUserId: PropTypes.string.isRequired, @@ -49,7 +51,6 @@ export default class Post extends PureComponent { isSearchResult: PropTypes.bool, commentedOnPost: PropTypes.object, managedConfig: PropTypes.object.isRequired, - navigator: PropTypes.object, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, shouldRenderReplyButton: PropTypes.bool, @@ -88,30 +89,16 @@ export default class Post extends PureComponent { goToUserProfile = () => { const {intl} = this.context; - const {navigator, post, theme} = this.props; - const options = { - screen: 'UserProfile', - title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}), - animated: true, - backButtonTitle: '', - passProps: { - userId: post.user_id, - }, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, + const {actions, post} = this.props; + const screen = 'UserProfile'; + const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); + const passProps = { + userId: post.user_id, }; Keyboard.dismiss(); requestAnimationFrame(() => { - if (Platform.OS === 'ios') { - navigator.push(options); - } else { - navigator.showModal(options); - } + actions.goToScreen(screen, title, passProps); }); }; @@ -120,7 +107,8 @@ export default class Post extends PureComponent { }; handleFailedPostPress = () => { - const options = { + const screen = 'OptionsModal'; + const passProps = { title: { id: t('mobile.post.failed_title'), defaultMessage: 'Unable to send your message:', @@ -151,22 +139,7 @@ export default class Post extends PureComponent { }], }; - 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', - }, - }); + this.props.actions.showModalOverCurrentContext(screen, passProps); }; handlePress = preventDoubleTap(() => { @@ -352,7 +325,6 @@ export default class Post extends PureComponent { channelIsReadOnly={channelIsReadOnly} isLastPost={isLastPost} isSearchResult={isSearchResult} - navigator={this.props.navigator} onFailedPostPress={this.handleFailedPostPress} onHashtagPress={onHashtagPress} onPermalinkPress={onPermalinkPress} diff --git a/app/components/post_add_channel_member/post_add_channel_member.js b/app/components/post_add_channel_member/post_add_channel_member.js index 84fa69f37..de79f312f 100644 --- a/app/components/post_add_channel_member/post_add_channel_member.js +++ b/app/components/post_add_channel_member/post_add_channel_member.js @@ -31,7 +31,6 @@ export default class PostAddChannelMember extends React.PureComponent { userIds: PropTypes.array.isRequired, usernames: PropTypes.array.isRequired, noGroupsUsernames: PropTypes.array, - navigator: PropTypes.object.isRequired, onPostPress: PropTypes.func, textStyles: PropTypes.object, }; @@ -90,7 +89,6 @@ export default class PostAddChannelMember extends React.PureComponent { mentionStyle={this.props.textStyles.mention} mentionName={usernames[0]} onPostPress={this.props.onPostPress} - navigator={this.props.navigator} /> ); } else if (usernames.length > 1) { @@ -119,7 +117,6 @@ export default class PostAddChannelMember extends React.PureComponent { mentionStyle={this.props.textStyles.mention} mentionName={username} onPostPress={this.props.onPostPress} - navigator={this.props.navigator} /> ); }).reduce((acc, el, idx, arr) => { diff --git a/app/components/post_attachment_opengraph/index.js b/app/components/post_attachment_opengraph/index.js index f422ddf5e..d18e1585b 100644 --- a/app/components/post_attachment_opengraph/index.js +++ b/app/components/post_attachment_opengraph/index.js @@ -6,6 +6,8 @@ import {bindActionCreators} from 'redux'; import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts'; +import {showModalOverCurrentContext} from 'app/actions/navigation'; + import {getDimensions} from 'app/selectors/device'; import PostAttachmentOpenGraph from './post_attachment_opengraph'; @@ -20,6 +22,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ getOpenGraphMetadata, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/components/post_attachment_opengraph/post_attachment_opengraph.js b/app/components/post_attachment_opengraph/post_attachment_opengraph.js index a495f159c..cb7d413ef 100644 --- a/app/components/post_attachment_opengraph/post_attachment_opengraph.js +++ b/app/components/post_attachment_opengraph/post_attachment_opengraph.js @@ -28,13 +28,13 @@ export default class PostAttachmentOpenGraph extends PureComponent { static propTypes = { actions: PropTypes.shape({ getOpenGraphMetadata: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }).isRequired, deviceHeight: PropTypes.number.isRequired, deviceWidth: PropTypes.number.isRequired, imagesMetadata: PropTypes.object, isReplyPost: PropTypes.bool, link: PropTypes.string.isRequired, - navigator: PropTypes.object.isRequired, openGraphData: PropTypes.object, theme: PropTypes.object.isRequired, }; @@ -195,6 +195,7 @@ export default class PostAttachmentOpenGraph extends PureComponent { originalWidth, originalHeight, } = this.state; + const {actions} = this.props; const filename = this.getFilename(link); const files = [{ @@ -209,7 +210,7 @@ export default class PostAttachmentOpenGraph extends PureComponent { }, }]; - previewImageAtIndex(this.props.navigator, [this.refs.item], 0, files); + previewImageAtIndex([this.refs.item], 0, files, actions.showModalOverCurrentContext); }; renderDescription = () => { diff --git a/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js b/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js index d86e41e65..dc1b926ff 100644 --- a/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js +++ b/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js @@ -25,6 +25,7 @@ describe('PostAttachmentOpenGraph', () => { const baseProps = { actions: { getOpenGraphMetadata: jest.fn(), + showModalOverCurrentContext: jest.fn(), }, deviceHeight: 600, deviceWidth: 400, @@ -36,7 +37,6 @@ describe('PostAttachmentOpenGraph', () => { }, isReplyPost: false, link: 'https://mattermost.com/', - navigator: {}, theme: Preferences.THEMES.default, }; diff --git a/app/components/post_body/index.js b/app/components/post_body/index.js index 6cbe0507a..5d4b9ef26 100644 --- a/app/components/post_body/index.js +++ b/app/components/post_body/index.js @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {General, Posts} from 'mattermost-redux/constants'; @@ -21,6 +22,8 @@ import { } from 'mattermost-redux/utils/post_utils'; import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils'; +import {showModalOverCurrentContext} from 'app/actions/navigation'; + import {getDimensions} from 'app/selectors/device'; import {hasEmojisOnly} from 'app/utils/emoji_utils'; @@ -102,4 +105,12 @@ function makeMapStateToProps() { }; } -export default connect(makeMapStateToProps, null, null, {forwardRef: true})(PostBody); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + showModalOverCurrentContext, + }, dispatch), + }; +} + +export default connect(makeMapStateToProps, mapDispatchToProps, null, {forwardRef: true})(PostBody); diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js index 018905393..26e279fe3 100644 --- a/app/components/post_body/post_body.js +++ b/app/components/post_body/post_body.js @@ -36,6 +36,9 @@ const SHOW_MORE_HEIGHT = 60; export default class PostBody extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + showModalOverCurrentContext: PropTypes.func.isRequired, + }).isRequired, canDelete: PropTypes.bool, channelIsReadOnly: PropTypes.bool.isRequired, deviceHeight: PropTypes.number.isRequired, @@ -56,7 +59,6 @@ export default class PostBody extends PureComponent { metadata: PropTypes.object, managedConfig: PropTypes.object, message: PropTypes.string, - navigator: PropTypes.object.isRequired, onFailedPostPress: PropTypes.func, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, @@ -130,31 +132,25 @@ export default class PostBody extends PureComponent { openLongPost = preventDoubleTap(() => { const { managedConfig, - navigator, onHashtagPress, onPermalinkPress, post, + actions, } = this.props; - + const screen = 'LongPost'; + const passProps = { + postId: post.id, + managedConfig, + onHashtagPress, + onPermalinkPress, + }; const options = { - screen: 'LongPost', - animationType: 'none', - backButtonTitle: '', - overrideBackPress: true, - navigatorStyle: { - navBarHidden: true, - screenBackgroundColor: changeOpacity('#000', 0.2), - modalPresentationStyle: 'overCurrentContext', - }, - passProps: { - postId: post.id, - managedConfig, - onHashtagPress, - onPermalinkPress, + layout: { + backgroundColor: changeOpacity('#000', 0.2), }, }; - navigator.showModal(options); + actions.showModalOverCurrentContext(screen, passProps, options); }); showPostOptions = () => { @@ -168,10 +164,10 @@ export default class PostBody extends PureComponent { isPostEphemeral, isSystemMessage, managedConfig, - navigator, post, showAddReaction, location, + actions, } = this.props; if (isSystemMessage && (!canDelete || hasBeenDeleted)) { @@ -182,32 +178,22 @@ export default class PostBody extends PureComponent { return; } - const options = { - screen: 'PostOptions', - animationType: 'none', - backButtonTitle: '', - navigatorStyle: { - navBarHidden: true, - navBarTransparent: true, - screenBackgroundColor: 'transparent', - modalPresentationStyle: 'overCurrentContext', - }, - passProps: { - canDelete, - channelIsReadOnly, - hasBeenDeleted, - isFlagged, - isSystemMessage, - post, - managedConfig, - showAddReaction, - location, - }, + const screen = 'PostOptions'; + const passProps = { + canDelete, + channelIsReadOnly, + hasBeenDeleted, + isFlagged, + isSystemMessage, + post, + managedConfig, + showAddReaction, + location, }; Keyboard.dismiss(); requestAnimationFrame(() => { - navigator.showModal(options); + actions.showModalOverCurrentContext(screen, passProps); }); }; @@ -231,7 +217,6 @@ export default class PostBody extends PureComponent { return ( ); } @@ -281,7 +264,6 @@ export default class PostBody extends PureComponent { isSystemMessage, message, metadata, - navigator, onHashtagPress, onPermalinkPress, post, @@ -304,7 +286,6 @@ export default class PostBody extends PureComponent { ); }; @@ -356,7 +335,6 @@ export default class PostBody extends PureComponent { isSystemMessage, message, metadata, - navigator, onFailedPostPress, onHashtagPress, onPermalinkPress, @@ -395,7 +373,6 @@ export default class PostBody extends PureComponent { allUsernames={allUsernames} linkStyle={textStyles.link} messageData={messageData} - navigator={navigator} textStyles={textStyles} theme={theme} /> @@ -424,7 +401,6 @@ export default class PostBody extends PureComponent { isEdited={hasBeenEdited} isReplyPost={isReplyPost} isSearchResult={isSearchResult} - navigator={navigator} onHashtagPress={onHashtagPress} onPermalinkPress={onPermalinkPress} onPostPress={onPress} diff --git a/app/components/post_body/post_body.test.js b/app/components/post_body/post_body.test.js index aa78520cb..94dcbc48c 100644 --- a/app/components/post_body/post_body.test.js +++ b/app/components/post_body/post_body.test.js @@ -12,6 +12,9 @@ import PostBody from './post_body.js'; describe('PostBody', () => { const baseProps = { + actions: { + showModalOverCurrentContext: jest.fn(), + }, canDelete: true, channelIsReadOnly: false, deviceHeight: 1920, @@ -30,7 +33,6 @@ describe('PostBody', () => { isSystemMessage: false, managedConfig: {}, message: 'Hello, World!', - navigator: {}, onFailedPostPress: jest.fn(), onHashtagPress: jest.fn(), onPermalinkPress: jest.fn(), diff --git a/app/components/post_body_additional_content/index.js b/app/components/post_body_additional_content/index.js index 115ecb98a..e4ace1dc0 100644 --- a/app/components/post_body_additional_content/index.js +++ b/app/components/post_body_additional_content/index.js @@ -10,6 +10,7 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getOpenGraphMetadataForUrl} from 'mattermost-redux/selectors/entities/posts'; import {getBool, getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {showModalOverCurrentContext} from 'app/actions/navigation'; import {ViewTypes} from 'app/constants'; import {getDimensions} from 'app/selectors/device'; import {extractFirstLink} from 'app/utils/url'; @@ -75,6 +76,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ getRedirectLocation, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js index 5358c7695..f5b311d16 100644 --- a/app/components/post_body_additional_content/post_body_additional_content.js +++ b/app/components/post_body_additional_content/post_body_additional_content.js @@ -38,6 +38,7 @@ export default class PostBodyAdditionalContent extends PureComponent { static propTypes = { actions: PropTypes.shape({ getRedirectLocation: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }).isRequired, baseTextStyle: CustomPropTypes.Style, blockStyles: PropTypes.object, @@ -48,7 +49,6 @@ export default class PostBodyAdditionalContent extends PureComponent { isReplyPost: PropTypes.bool, link: PropTypes.string, message: PropTypes.string.isRequired, - navigator: PropTypes.object.isRequired, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, openGraphData: PropTypes.object, @@ -182,7 +182,7 @@ export default class PostBodyAdditionalContent extends PureComponent { }; generateStaticEmbed = (isYouTube, isImage) => { - const {isReplyPost, link, metadata, navigator, openGraphData, showLinkPreviews, theme} = this.props; + const {isReplyPost, link, metadata, openGraphData, showLinkPreviews, theme} = this.props; if (isYouTube || (isImage && !openGraphData)) { return null; @@ -206,7 +206,6 @@ export default class PostBodyAdditionalContent extends PureComponent { { const {shortenedLink} = this.state; let {link} = this.props; - const {navigator} = this.props; + const {actions} = this.props; if (shortenedLink) { link = shortenedLink; } @@ -435,7 +432,7 @@ export default class PostBodyAdditionalContent extends PureComponent { }, }]; - previewImageAtIndex(navigator, [imageRef], 0, files); + previewImageAtIndex([imageRef], 0, files, actions.showModalOverCurrentContext); }; playYouTubeVideo = () => { diff --git a/app/components/post_list/index.js b/app/components/post_list/index.js index 92168b4ba..1c8f430ef 100644 --- a/app/components/post_list/index.js +++ b/app/components/post_list/index.js @@ -9,6 +9,7 @@ import {getConfig, getCurrentUrl} from 'mattermost-redux/selectors/entities/gene import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from 'mattermost-redux/utils/post_list'; +import {showModalOverCurrentContext} from 'app/actions/navigation'; import {handleSelectChannelByName, loadChannelsByTeamName, refreshChannelWithRetry} from 'app/actions/views/channel'; import {setDeepLinkURL} from 'app/actions/views/root'; @@ -42,6 +43,7 @@ function mapDispatchToProps(dispatch) { refreshChannelWithRetry, selectFocusedPostId, setDeepLinkURL, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 8634cf44e..262c0d373 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -41,6 +41,7 @@ export default class PostList extends PureComponent { refreshChannelWithRetry: PropTypes.func.isRequired, selectFocusedPostId: PropTypes.func.isRequired, setDeepLinkURL: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }).isRequired, channelId: PropTypes.string, deepLinkURL: PropTypes.string, @@ -51,7 +52,6 @@ export default class PostList extends PureComponent { isSearchResult: PropTypes.bool, lastPostIndex: PropTypes.number.isRequired, lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types - navigator: PropTypes.object, onLoadMoreUp: PropTypes.func, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, @@ -250,7 +250,6 @@ export default class PostList extends PureComponent { isSearchResult: this.props.isSearchResult, location: this.props.location, managedConfig: mattermostManaged.getCachedConfig(), - navigator: this.props.navigator, onHashtagPress: this.props.onHashtagPress, onPermalinkPress: this.handlePermalinkPress, onPress: this.props.onPostPress, @@ -307,29 +306,24 @@ export default class PostList extends PureComponent { }; showPermalinkView = (postId) => { - const {actions, navigator} = this.props; + const {actions} = this.props; actions.selectFocusedPostId(postId); if (!this.showingPermalink) { + const screen = 'Permalink'; + const passProps = { + isPermalink: true, + onClose: this.handleClosePermalink, + }; const options = { - screen: 'Permalink', - animationType: 'none', - backButtonTitle: '', - overrideBackPress: true, - navigatorStyle: { - navBarHidden: true, - screenBackgroundColor: changeOpacity('#000', 0.2), - modalPresentationStyle: 'overCurrentContext', - }, - passProps: { - isPermalink: true, - onClose: this.handleClosePermalink, + layout: { + backgroundColor: changeOpacity('#000', 0.2), }, }; this.showingPermalink = true; - navigator.showModal(options); + actions.showModalOverCurrentContext(screen, passProps, options); } }; diff --git a/app/components/post_list/post_list.test.js b/app/components/post_list/post_list.test.js index 0c6455843..a4219bf89 100644 --- a/app/components/post_list/post_list.test.js +++ b/app/components/post_list/post_list.test.js @@ -18,11 +18,9 @@ describe('PostList', () => { refreshChannelWithRetry: jest.fn(), selectFocusedPostId: jest.fn(), setDeepLinkURL: jest.fn(), + showModalOverCurrentContext: jest.fn(), }, deepLinkURL: '', - navigator: { - showModal: jest.fn(), - }, lastPostIndex: -1, postIds: ['post-id-1', 'post-id-2'], serverURL, @@ -47,7 +45,7 @@ describe('PostList', () => { wrapper.setProps({deepLinkURL: deepLinks.permalink}); expect(baseProps.actions.setDeepLinkURL).toHaveBeenCalled(); expect(baseProps.actions.selectFocusedPostId).toHaveBeenCalled(); - expect(baseProps.navigator.showModal).toHaveBeenCalled(); + expect(baseProps.actions.showModalOverCurrentContext).toHaveBeenCalled(); expect(wrapper.getElement()).toMatchSnapshot(); }); diff --git a/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap b/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap index ac7535189..ea489c424 100644 --- a/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap +++ b/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap @@ -16,23 +16,11 @@ exports[`PostTextBox should match, full snapshot 1`] = ` } } > - { files: [], maxFileSize: 1024, maxMessageLength: 4000, - navigator: { - showModal: jest.fn(), - }, rootId: '', theme: Preferences.THEMES.default, uploadFileRequestStatus: 'NOT_STARTED', diff --git a/app/components/post_textbox/post_textbox_base.js b/app/components/post_textbox/post_textbox_base.js index 08ae4a1ae..f129c7d74 100644 --- a/app/components/post_textbox/post_textbox_base.js +++ b/app/components/post_textbox/post_textbox_base.js @@ -63,7 +63,6 @@ export default class PostTextBoxBase extends PureComponent { files: PropTypes.array, maxFileSize: PropTypes.number.isRequired, maxMessageLength: PropTypes.number.isRequired, - navigator: PropTypes.object, rootId: PropTypes.string, theme: PropTypes.object.isRequired, uploadFileRequestStatus: PropTypes.string.isRequired, @@ -189,7 +188,7 @@ export default class PostTextBoxBase extends PureComponent { }; getAttachmentButton = () => { - const {canUploadFiles, channelIsReadOnly, files, maxFileSize, navigator, theme} = this.props; + const {canUploadFiles, channelIsReadOnly, files, maxFileSize, theme} = this.props; let attachmentButton = null; if (canUploadFiles && !channelIsReadOnly) { @@ -197,7 +196,6 @@ export default class PostTextBoxBase extends PureComponent { { - const navigator = { - setOnNavigatorEvent: jest.fn(), - setButtons: jest.fn(), - dismissModal: jest.fn(), - push: jest.fn(), - }; - const baseProps = { theme: Preferences.THEMES.default, - navigator, currentUser: { first_name: 'Dwight', last_name: 'Schrute', diff --git a/app/components/reactions/index.js b/app/components/reactions/index.js index e778343b6..87c20b010 100644 --- a/app/components/reactions/index.js +++ b/app/components/reactions/index.js @@ -13,6 +13,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getChannel} from 'mattermost-redux/selectors/entities/channels'; +import {showModal, showModalOverCurrentContext} from 'app/actions/navigation'; import {addReaction} from 'app/actions/views/emoji'; import Reactions from './reactions'; @@ -63,6 +64,8 @@ function mapDispatchToProps(dispatch) { addReaction, getReactionsForPost, removeReaction, + showModal, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/components/reactions/reactions.js b/app/components/reactions/reactions.js index 3fead81a7..5548ed180 100644 --- a/app/components/reactions/reactions.js +++ b/app/components/reactions/reactions.js @@ -23,9 +23,10 @@ export default class Reactions extends PureComponent { addReaction: PropTypes.func.isRequired, getReactionsForPost: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, + showModal: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }).isRequired, currentUserId: PropTypes.string.isRequired, - navigator: PropTypes.object.isRequired, position: PropTypes.oneOf(['right', 'left']), postId: PropTypes.string.isRequired, reactions: PropTypes.object, @@ -50,25 +51,18 @@ export default class Reactions extends PureComponent { } handleAddReaction = preventDoubleTap(() => { + const {actions, theme} = this.props; const {formatMessage} = this.context.intl; - const {navigator, theme} = this.props; + const screen = 'AddReaction'; + const title = formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}); MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => { - navigator.showModal({ - screen: 'AddReaction', - title: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - closeButton: source, - onEmojiPress: this.handleAddReactionToPost, - }, - }); + const passProps = { + closeButton: source, + onEmojiPress: this.handleAddReactionToPost, + }; + + actions.showModal(screen, title, passProps); }); }); @@ -87,28 +81,18 @@ export default class Reactions extends PureComponent { }; showReactionList = () => { - const {navigator, postId} = this.props; + const {actions, postId} = this.props; - const options = { - screen: 'ReactionList', - animationType: 'none', - backButtonTitle: '', - navigatorStyle: { - navBarHidden: true, - navBarTransparent: true, - screenBackgroundColor: 'transparent', - modalPresentationStyle: 'overCurrentContext', - }, - passProps: { - postId, - }, + const screen = 'ReactionList'; + const passProps = { + postId, }; - navigator.showModal(options); + actions.showModalOverCurrentContext(screen, passProps); } renderReactions = () => { - const {currentUserId, navigator, reactions, theme, postId} = this.props; + const {currentUserId, reactions, theme, postId} = this.props; const highlightedReactions = []; const reactionsByName = Object.values(reactions).reduce((acc, reaction) => { if (acc.has(reaction.emoji_name)) { @@ -131,7 +115,6 @@ export default class Reactions extends PureComponent { count={reactionsByName.get(r).length} emojiName={r} highlight={highlightedReactions.includes(r)} - navigator={navigator} onPress={this.handleReactionPress} onLongPress={this.showReactionList} postId={postId} diff --git a/app/components/root/index.js b/app/components/root/index.js index c2234531c..7f4b28031 100644 --- a/app/components/root/index.js +++ b/app/components/root/index.js @@ -1,13 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; - -import {getCurrentLocale} from 'app/selectors/i18n'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; + +import {resetToTeams} from 'app/actions/navigation'; +import {getCurrentLocale} from 'app/selectors/i18n'; import {removeProtocol} from 'app/utils/url'; import Root from './root'; @@ -17,10 +18,17 @@ function mapStateToProps(state) { return { theme: getTheme(state), - currentChannelId: getCurrentChannelId(state), currentUrl: removeProtocol(getCurrentUrl(state)), locale, }; } -export default connect(mapStateToProps)(Root); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + resetToTeams, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Root); diff --git a/app/components/root/root.js b/app/components/root/root.js index 5566e7335..95e46af44 100644 --- a/app/components/root/root.js +++ b/app/components/root/root.js @@ -9,15 +9,16 @@ import {Platform} from 'react-native'; import {Client4} from 'mattermost-redux/client'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {NavigationTypes, ViewTypes} from 'app/constants'; +import {NavigationTypes} from 'app/constants'; import {getTranslations} from 'app/i18n'; export default class Root extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + resetToTeams: PropTypes.func.isRequired, + }).isRequired, children: PropTypes.node, - navigator: PropTypes.object, excludeEvents: PropTypes.bool, - currentChannelId: PropTypes.string, currentUrl: PropTypes.string, locale: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, @@ -27,8 +28,6 @@ export default class Root extends PureComponent { Client4.setAcceptLanguage(this.props.locale); if (!this.props.excludeEvents) { - EventEmitter.on(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); - EventEmitter.on(ViewTypes.NOTIFICATION_TAPPED, this.handleNotificationTapped); EventEmitter.on(NavigationTypes.NAVIGATION_NO_TEAMS, this.handleNoTeams); EventEmitter.on(NavigationTypes.NAVIGATION_ERROR_TEAMS, this.errorTeamsList); } @@ -42,30 +41,11 @@ export default class Root extends PureComponent { componentWillUnmount() { if (!this.props.excludeEvents) { - EventEmitter.off(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); - EventEmitter.off(ViewTypes.NOTIFICATION_TAPPED, this.handleNotificationTapped); EventEmitter.off(NavigationTypes.NAVIGATION_NO_TEAMS, this.handleNoTeams); EventEmitter.off(NavigationTypes.NAVIGATION_ERROR_TEAMS, this.errorTeamsList); } } - handleInAppNotification = (notification) => { - const {data} = notification; - const {currentChannelId, navigator} = this.props; - - if (data && data.channel_id !== currentChannelId) { - navigator.showInAppNotification({ - screen: 'Notification', - position: 'top', - autoDismissTimerSec: 5, - dismissWithSwipe: true, - passProps: { - notification, - }, - }); - } - }; - handleNoTeams = () => { if (!this.refs.provider) { setTimeout(this.handleNoTeams, 200); @@ -83,28 +63,24 @@ export default class Root extends PureComponent { } navigateToTeamsPage = (screen) => { - const {currentUrl, navigator, theme} = this.props; + const {currentUrl, theme, actions} = this.props; const {intl} = this.refs.provider.getChildContext(); - let navigatorButtons; let passProps = {theme}; + const options = {topBar: {}}; if (Platform.OS === 'android') { - navigatorButtons = { - rightButtons: [{ - title: intl.formatMessage({id: 'sidebar_right_menu.logout', defaultMessage: 'Logout'}), - id: 'logout', - buttonColor: theme.sidebarHeaderTextColor, - showAsAction: 'always', - }], - }; + options.topBar.rightButtons = [{ + id: 'logout', + text: intl.formatMessage({id: 'sidebar_right_menu.logout', defaultMessage: 'Logout'}), + color: theme.sidebarHeaderTextColor, + showAsAction: 'always', + }]; } else { - navigatorButtons = { - leftButtons: [{ - title: intl.formatMessage({id: 'sidebar_right_menu.logout', defaultMessage: 'Logout'}), - id: 'logout', - buttonColor: theme.sidebarHeaderTextColor, - }], - }; + options.topBar.leftButtons = [{ + id: 'logout', + text: intl.formatMessage({id: 'sidebar_right_menu.logout', defaultMessage: 'Logout'}), + color: theme.sidebarHeaderTextColor, + }]; } if (screen === 'SelectTeam') { @@ -115,34 +91,11 @@ export default class Root extends PureComponent { }; } - navigator.resetTo({ - screen, - title: intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}), - animated: false, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - navigatorButtons, - passProps, - }); + const title = intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}); + + actions.resetToTeams(screen, title, passProps, options); } - handleNotificationTapped = async () => { - const {navigator} = this.props; - - if (Platform.OS === 'android') { - navigator.dismissModal({animation: 'none'}); - } - - navigator.popToRoot({ - animated: false, - }); - }; - render() { const locale = this.props.locale; diff --git a/app/components/safe_area_view/safe_area_view.ios.js b/app/components/safe_area_view/safe_area_view.ios.js index 73f79a00f..00d73702a 100644 --- a/app/components/safe_area_view/safe_area_view.ios.js +++ b/app/components/safe_area_view/safe_area_view.ios.js @@ -23,7 +23,6 @@ export default class SafeAreaIos extends PureComponent { forceTop: PropTypes.number, keyboardOffset: PropTypes.number.isRequired, navBarBackgroundColor: PropTypes.string, - navigator: PropTypes.object, headerComponent: PropTypes.node, theme: PropTypes.object.isRequired, }; @@ -35,10 +34,6 @@ export default class SafeAreaIos extends PureComponent { constructor(props) { super(props); - if (props.navigator) { - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - } - this.state = { keyboard: false, safeAreaInsets: { @@ -110,15 +105,6 @@ export default class SafeAreaIos extends PureComponent { this.setState({keyboard: true}); }; - onNavigatorEvent = (event) => { - switch (event.id) { - case 'willAppear': - case 'didDisappear': - this.getSafeAreaInsets(); - break; - } - }; - renderTopBar = () => { const {safeAreaInsets, statusBarHeight} = this.state; const {headerComponent, excludeHeader, forceTop, navBarBackgroundColor, theme} = this.props; diff --git a/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap b/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap index 8a859004b..1bfdb0263 100644 --- a/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap +++ b/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap @@ -2,11 +2,10 @@ exports[`ChannelItem should match snapshot 1`] = ` - - + `; exports[`ChannelItem should match snapshot for current user i.e currentUser (you) 1`] = ` - - + `; exports[`ChannelItem should match snapshot for current user i.e currentUser (you) when isSearchResult 1`] = ` - - + `; exports[`ChannelItem should match snapshot for deactivated user and is currentChannel 1`] = ` - - + `; exports[`ChannelItem should match snapshot for deactivated user and is searchResult 1`] = ` - - + `; @@ -551,11 +546,10 @@ exports[`ChannelItem should match snapshot for showUnreadForMsgs 1`] = `null`; exports[`ChannelItem should match snapshot with draft 1`] = ` - - + `; exports[`ChannelItem should match snapshot with mentions and muted 1`] = ` - - + `; diff --git a/app/components/sidebars/main/channels_list/channel_item/channel_item.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.js index e1ab8a03a..e5af091d4 100644 --- a/app/components/sidebars/main/channels_list/channel_item/channel_item.js +++ b/app/components/sidebars/main/channels_list/channel_item/channel_item.js @@ -2,17 +2,17 @@ // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; -import {General} from 'mattermost-redux/constants'; - import PropTypes from 'prop-types'; import { Animated, - Platform, TouchableHighlight, Text, View, } from 'react-native'; import {intlShape} from 'react-intl'; +import {Navigation} from 'react-native-navigation'; + +import {General} from 'mattermost-redux/constants'; import Badge from 'app/components/badge'; import ChannelIcon from 'app/components/channel_icon'; @@ -32,7 +32,6 @@ export default class ChannelItem extends PureComponent { isUnread: PropTypes.bool, hasDraft: PropTypes.bool, mentions: PropTypes.number.isRequired, - navigator: PropTypes.object, onSelectChannel: PropTypes.func.isRequired, shouldHideChannel: PropTypes.bool, showUnreadForMsgs: PropTypes.bool.isRequired, @@ -40,6 +39,7 @@ export default class ChannelItem extends PureComponent { unreadMsgs: PropTypes.number.isRequired, isSearchResult: PropTypes.bool, isBot: PropTypes.bool.isRequired, + previewChannel: PropTypes.func, }; static defaultProps = { @@ -58,28 +58,25 @@ export default class ChannelItem extends PureComponent { }); }); - onPreview = () => { - const {channelId, navigator} = this.props; - if (Platform.OS === 'ios' && navigator && this.previewRef) { + onPreview = ({reactTag}) => { + const {channelId, previewChannel} = this.props; + if (previewChannel) { const {intl} = this.context; - - navigator.push({ - screen: 'ChannelPeek', - previewCommit: false, - previewView: this.previewRef, - previewActions: [{ - id: 'action-mark-as-read', - title: intl.formatMessage({id: 'mobile.channel.markAsRead', defaultMessage: 'Mark As Read'}), - }], - passProps: { - channelId, + const passProps = { + channelId, + }; + const options = { + preview: { + reactTag, + actions: [{ + id: 'action-mark-as-read', + title: intl.formatMessage({id: 'mobile.channel.markAsRead', defaultMessage: 'Mark As Read'}), + }], }, - }); - } - }; + }; - setPreviewRef = (ref) => { - this.previewRef = ref; + previewChannel(passProps, options); + } }; showChannelAsUnread = () => { @@ -190,11 +187,12 @@ export default class ChannelItem extends PureComponent { ); return ( - - + {extraBorder} @@ -210,7 +208,7 @@ export default class ChannelItem extends PureComponent { {badge} - + ); } diff --git a/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js index 8774c4614..b9ced6211 100644 --- a/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js +++ b/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js @@ -3,7 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import {TouchableHighlight} from 'react-native'; +import {Navigation} from 'react-native-navigation'; import Preferences from 'mattermost-redux/constants/preferences'; @@ -31,7 +31,6 @@ describe('ChannelItem', () => { isUnread: true, hasDraft: false, mentions: 0, - navigator: {push: () => {}}, // eslint-disable-line no-empty-function onSelectChannel: () => {}, // eslint-disable-line no-empty-function shouldHideChannel: false, showUnreadForMsgs: true, @@ -216,7 +215,7 @@ describe('ChannelItem', () => { {context: {intl: {formatMessage: jest.fn()}}}, ); - wrapper.find(TouchableHighlight).simulate('press'); + wrapper.find(Navigation.TouchablePreview).simulate('press'); jest.runAllTimers(); const expectedChannelParams = {id: baseProps.channelId, display_name: baseProps.displayName, fake: channel.fake, type: channel.type}; diff --git a/app/components/sidebars/main/channels_list/channels_list.js b/app/components/sidebars/main/channels_list/channels_list.js index 4d5ec4d15..1a26c82f0 100644 --- a/app/components/sidebars/main/channels_list/channels_list.js +++ b/app/components/sidebars/main/channels_list/channels_list.js @@ -22,7 +22,6 @@ let FilteredList = null; export default class ChannelsList extends PureComponent { static propTypes = { - navigator: PropTypes.object, onJoinChannel: PropTypes.func.isRequired, onSearchEnds: PropTypes.func.isRequired, onSearchStart: PropTypes.func.isRequired, @@ -30,6 +29,7 @@ export default class ChannelsList extends PureComponent { onShowTeams: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, drawerOpened: PropTypes.bool, + previewChannel: PropTypes.func, }; static contextTypes = { @@ -86,9 +86,9 @@ export default class ChannelsList extends PureComponent { render() { const {intl} = this.context; const { - navigator, onShowTeams, theme, + previewChannel, } = this.props; const {searching, term} = this.state; @@ -101,14 +101,15 @@ export default class ChannelsList extends PureComponent { onSelectChannel={this.onSelectChannel} styles={styles} term={term} + previewChannel={previewChannel} /> ); } else { list = ( ); } diff --git a/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js b/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js index 7546f3630..d37d56c42 100644 --- a/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js +++ b/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js @@ -54,6 +54,7 @@ class FilteredList extends Component { styles: PropTypes.object.isRequired, term: PropTypes.string, theme: PropTypes.object.isRequired, + previewChannel: PropTypes.func, }; static defaultProps = { @@ -126,6 +127,7 @@ class FilteredList extends Component { isUnread={channel.isUnread} mentions={0} onSelectChannel={this.onSelectChannel} + previewChannel={this.props.previewChannel} /> ); }; diff --git a/app/components/sidebars/main/channels_list/list/index.js b/app/components/sidebars/main/channels_list/list/index.js index adb6f31a7..eaca23964 100644 --- a/app/components/sidebars/main/channels_list/list/index.js +++ b/app/components/sidebars/main/channels_list/list/index.js @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {General} from 'mattermost-redux/constants'; @@ -17,6 +18,8 @@ import {memoizeResult} from 'mattermost-redux/utils/helpers'; import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {showModal} from 'app/actions/navigation'; + import {DeviceTypes, ViewTypes} from 'app/constants'; import List from './list'; @@ -67,6 +70,14 @@ function mapStateToProps(state) { }; } +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + showModal, + }, dispatch), + }; +} + function areStatesEqual(next, prev) { const equalRoles = getCurrentUserRoles(prev) === getCurrentUserRoles(next); const equalChannels = next.entities.channels === prev.entities.channels; @@ -77,4 +88,4 @@ function areStatesEqual(next, prev) { return equalChannels && equalConfig && equalRoles && equalUsers && equalFav; } -export default connect(mapStateToProps, null, null, {pure: true, areStatesEqual})(List); +export default connect(mapStateToProps, mapDispatchToProps, null, {pure: true, areStatesEqual})(List); diff --git a/app/components/sidebars/main/channels_list/list/list.js b/app/components/sidebars/main/channels_list/list/list.js index 27ce989e1..9e0e3101f 100644 --- a/app/components/sidebars/main/channels_list/list/list.js +++ b/app/components/sidebars/main/channels_list/list/list.js @@ -34,14 +34,17 @@ let UnreadIndicator = null; export default class List extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + showModal: PropTypes.func.isRequired, + }).isRequired, canCreatePrivateChannels: PropTypes.bool.isRequired, favoriteChannelIds: PropTypes.array.isRequired, - navigator: PropTypes.object, onSelectChannel: PropTypes.func.isRequired, unreadChannelIds: PropTypes.array.isRequired, styles: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, orderedChannelIds: PropTypes.array.isRequired, + previewChannel: PropTypes.func, }; static contextTypes = { @@ -159,7 +162,10 @@ export default class List extends PureComponent { }; showCreateChannelOptions = () => { - const {canCreatePrivateChannels, navigator} = this.props; + const { + canCreatePrivateChannels, + actions, + } = this.props; const items = []; const moreChannels = { @@ -197,117 +203,87 @@ export default class List extends PureComponent { } items.push(newConversation); - navigator.showModal({ - screen: 'OptionsModal', - title: '', - animationType: 'none', - passProps: { - items, - onItemPress: () => navigator.dismissModal({ - animationType: 'none', - }), + const screen = 'OptionsModal'; + const title = ''; + const passProps = { + items, + }; + const options = { + modalPresentationStyle: 'overCurrentContext', + layout: { + backgroundColor: 'transparent', }, - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - modalPresentationStyle: 'overCurrentContext', + topBar: { + visible: false, + height: 0, }, - }); + animations: { + showModal: { + enable: false, + }, + dismissModal: { + enable: false, + }, + }, + }; + + actions.showModal(screen, title, passProps, options); }; goToCreatePublicChannel = preventDoubleTap(() => { - const {navigator, theme} = this.props; + const {actions} = this.props; const {intl} = this.context; + const screen = 'CreateChannel'; + const title = intl.formatMessage({id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'}); + const passProps = { + channelType: General.OPEN_CHANNEL, + closeButton: this.closeButton, + }; - navigator.showModal({ - screen: 'CreateChannel', - animationType: 'slide-up', - title: intl.formatMessage({id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'}), - backButtonTitle: '', - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelType: General.OPEN_CHANNEL, - closeButton: this.closeButton, - }, - }); + actions.showModal(screen, title, passProps); }); goToCreatePrivateChannel = preventDoubleTap(() => { - const {navigator, theme} = this.props; + const {actions} = this.props; const {intl} = this.context; + const screen = 'CreateChannel'; + const title = intl.formatMessage({id: 'mobile.create_channel.private', defaultMessage: 'New Private Channel'}); + const passProps = { + channelType: General.PRIVATE_CHANNEL, + closeButton: this.closeButton, + }; - 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, - }, - }); + actions.showModal(screen, title, passProps); }); goToDirectMessages = preventDoubleTap(() => { - const {navigator, theme} = this.props; + const {actions} = this.props; const {intl} = this.context; - - navigator.showModal({ - screen: 'MoreDirectMessages', - title: intl.formatMessage({id: 'mobile.more_dms.title', defaultMessage: 'New Conversation'}), - animationType: 'slide-up', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - navigatorButtons: { + const screen = 'MoreDirectMessages'; + const title = intl.formatMessage({id: 'mobile.more_dms.title', defaultMessage: 'New Conversation'}); + const passProps = {}; + const options = { + topBar: { leftButtons: [{ id: 'close-dms', icon: this.closeButton, }], }, - }); + }; + + actions.showModal(screen, title, passProps, options); }); goToMoreChannels = preventDoubleTap(() => { - const {navigator, theme} = this.props; + const {actions} = this.props; const {intl} = this.context; + const screen = 'MoreChannels'; + const title = intl.formatMessage({id: 'more_channels.title', defaultMessage: 'More Channels'}); + const passProps = { + closeButton: this.closeButton, + }; - 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: { - closeButton: this.closeButton, - }, - }); + actions.showModal(screen, title, passProps); }); keyExtractor = (item) => item.id || item; @@ -349,15 +325,15 @@ export default class List extends PureComponent { }; renderItem = ({item}) => { - const {favoriteChannelIds, unreadChannelIds} = this.props; + const {favoriteChannelIds, unreadChannelIds, previewChannel} = this.props; return ( ); }; diff --git a/app/components/sidebars/main/main_sidebar.js b/app/components/sidebars/main/main_sidebar.js index d85512604..edb8d6ddd 100644 --- a/app/components/sidebars/main/main_sidebar.js +++ b/app/components/sidebars/main/main_sidebar.js @@ -46,9 +46,9 @@ export default class ChannelSidebar extends Component { currentUserId: PropTypes.string.isRequired, deviceWidth: PropTypes.number.isRequired, isLandscape: PropTypes.bool.isRequired, - navigator: PropTypes.object, teamsCount: PropTypes.number.isRequired, theme: PropTypes.object.isRequired, + previewChannel: PropTypes.func, }; static contextTypes = { @@ -143,7 +143,7 @@ export default class ChannelSidebar extends Component { }; handleShowDrawerContent = () => { - this.setState({show: true}); + requestAnimationFrame(() => this.setState({show: true})); }; closeChannelDrawer = () => { @@ -315,9 +315,9 @@ export default class ChannelSidebar extends Component { renderNavigationView = (drawerWidth) => { const { - navigator, teamsCount, theme, + previewChannel, } = this.props; const { @@ -349,7 +349,6 @@ export default class ChannelSidebar extends Component { > ); @@ -363,7 +362,6 @@ export default class ChannelSidebar extends Component { > ); @@ -380,7 +379,6 @@ export default class ChannelSidebar extends Component { navBarBackgroundColor={theme.sidebarBg} backgroundColor={theme.sidebarHeaderBg} footerColor={theme.sidebarHeaderBg} - navigator={navigator} > { const {intl} = this.context; - const {currentUrl, navigator, theme} = this.props; - - navigator.showModal({ - screen: 'SelectTeam', - title: intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}), - animationType: 'slide-up', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - navigatorButtons: { + const {currentUrl, theme, actions} = this.props; + const screen = 'SelectTeam'; + const title = intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}); + const passProps = { + currentUrl, + theme, + }; + const options = { + topBar: { leftButtons: [{ id: 'close-teams', icon: this.closeButton, }], }, - passProps: { - currentUrl, - theme, - }, - }); + }; + + actions.showModal(screen, title, passProps, options); }); keyExtractor = (item) => { diff --git a/app/components/sidebars/settings/index.js b/app/components/sidebars/settings/index.js index 140b697d0..66406da3f 100644 --- a/app/components/sidebars/settings/index.js +++ b/app/components/sidebars/settings/index.js @@ -8,6 +8,12 @@ import {logout, setStatus} from 'mattermost-redux/actions/users'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; +import { + showModal, + showModalOverCurrentContext, + dismissModal, +} from 'app/actions/navigation'; + import {isLandscape, getDimensions} from 'app/selectors/device'; import SettingsSidebar from './settings_sidebar'; @@ -30,6 +36,9 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ logout, setStatus, + showModal, + showModalOverCurrentContext, + dismissModal, }, dispatch), }; } diff --git a/app/components/sidebars/settings/settings_sidebar.js b/app/components/sidebars/settings/settings_sidebar.js index 9d552978b..41a76f970 100644 --- a/app/components/sidebars/settings/settings_sidebar.js +++ b/app/components/sidebars/settings/settings_sidebar.js @@ -37,13 +37,15 @@ export default class SettingsDrawer extends PureComponent { actions: PropTypes.shape({ logout: PropTypes.func.isRequired, setStatus: PropTypes.func.isRequired, + showModal: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, }).isRequired, blurPostTextBox: PropTypes.func.isRequired, children: PropTypes.node, currentUser: PropTypes.object.isRequired, deviceWidth: PropTypes.number.isRequired, isLandscape: PropTypes.bool.isRequired, - navigator: PropTypes.object, status: PropTypes.string, theme: PropTypes.object.isRequired, }; @@ -66,10 +68,12 @@ export default class SettingsDrawer extends PureComponent { } componentDidMount() { + EventEmitter.on('close_settings_sidebar', this.closeSettingsSidebar); BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack); } componentWillUnmount() { + EventEmitter.off('close_settings_sidebar', this.closeSettingsSidebar); BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack); } @@ -107,6 +111,7 @@ export default class SettingsDrawer extends PureComponent { }; handleSetStatus = preventDoubleTap(() => { + const {actions} = this.props; const items = [{ action: () => this.setStatus(General.ONLINE), text: { @@ -133,21 +138,7 @@ export default class SettingsDrawer extends PureComponent { }, }]; - this.props.navigator.showModal({ - screen: 'OptionsModal', - title: '', - animationType: 'none', - passProps: { - items, - }, - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - modalPresentationStyle: 'overCurrentContext', - }, - }); + actions.showModalOverCurrentContext('OptionsModal', {items}); }); goToEditProfile = preventDoubleTap(() => { @@ -207,31 +198,20 @@ export default class SettingsDrawer extends PureComponent { }); openModal = (screen, title, passProps) => { - const {navigator, theme} = this.props; - this.closeSettingsSidebar(); + const {actions} = this.props; + const options = { + topBar: { + leftButtons: [{ + id: 'close-settings', + icon: this.closeButton, + }], + }, + }; + InteractionManager.runAfterInteractions(() => { - navigator.showModal({ - screen, - title, - 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, - }], - }, - passProps, - }); + actions.showModal(screen, title, passProps, options); }); }; @@ -251,7 +231,7 @@ export default class SettingsDrawer extends PureComponent { }; renderNavigationView = () => { - const {currentUser, navigator, theme} = this.props; + const {currentUser, theme} = this.props; const style = getStyleSheet(theme); return ( @@ -261,7 +241,6 @@ export default class SettingsDrawer extends PureComponent { footerColor={theme.centerChannelBg} footerComponent={} headerComponent={} - navigator={navigator} theme={theme} > @@ -356,12 +335,10 @@ export default class SettingsDrawer extends PureComponent { }; setStatus = (status) => { - const {status: currentUserStatus, navigator} = this.props; + const {status: currentUserStatus, actions} = this.props; if (currentUserStatus === General.OUT_OF_OFFICE) { - navigator.dismissModal({ - animationType: 'none', - }); + actions.dismissModal(); this.closeSettingsSidebar(); this.confirmReset(status); return; diff --git a/app/constants/navigation.js b/app/constants/navigation.js index 809997da3..618dc5b3f 100644 --- a/app/constants/navigation.js +++ b/app/constants/navigation.js @@ -9,6 +9,7 @@ const NavigationTypes = keyMirror({ NAVIGATION_NO_TEAMS: null, RESTART_APP: null, NAVIGATION_ERROR_TEAMS: null, + NAVIGATION_SHOW_OVERLAY: null, }); export default NavigationTypes; diff --git a/app/constants/view.js b/app/constants/view.js index 42563da24..010944a69 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -42,7 +42,6 @@ const ViewTypes = keyMirror({ COMMENT_DRAFT_SELECTION_CHANGED: null, NOTIFICATION_IN_APP: null, - NOTIFICATION_TAPPED: null, SET_POST_DRAFT: null, SET_COMMENT_DRAFT: null, @@ -87,9 +86,6 @@ const ViewTypes = keyMirror({ INCREMENT_EMOJI_PICKER_PAGE: null, - LAUNCH_LOGIN: null, - LAUNCH_CHANNEL: null, - SET_DEEP_LINK_URL: null, SET_PROFILE_IMAGE_URI: null, diff --git a/app/init/credentials.js b/app/init/credentials.js index 0f4ee7753..14b401467 100644 --- a/app/init/credentials.js +++ b/app/init/credentials.js @@ -7,7 +7,7 @@ import * as KeyChain from 'react-native-keychain'; import {Client4} from 'mattermost-redux/client'; import mattermostManaged from 'app/mattermost_managed'; -import ephemeralStore from 'app/store/ephemeral_store'; +import EphemeralStore from 'app/store/ephemeral_store'; import {setCSRFFromCookie} from 'app/utils/security'; const CURRENT_SERVER = '@currentServerUrl'; @@ -26,7 +26,7 @@ export const setAppCredentials = (deviceToken, currentUserId, token, url) => { try { const username = `${deviceToken}, ${currentUserId}`; - ephemeralStore.deviceToken = deviceToken; + EphemeralStore.deviceToken = deviceToken; AsyncStorage.setItem(CURRENT_SERVER, url); KeyChain.setInternetCredentials(url, username, token, {accessGroup: mattermostManaged.appGroupIdentifier}); } catch (e) { @@ -111,7 +111,7 @@ async function getInternetCredentials(url) { const [deviceToken, currentUserId] = usernameParsed; if (token && token !== 'undefined') { - ephemeralStore.deviceToken = deviceToken; + EphemeralStore.deviceToken = deviceToken; Client4.setUserId(currentUserId); Client4.setUrl(url); Client4.setToken(token); diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js index 73bc3d04c..4eb258b93 100644 --- a/app/init/global_event_handler.js +++ b/app/init/global_event_handler.js @@ -11,11 +11,13 @@ import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; import {Client4} from 'mattermost-redux/client'; import {General} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device'; import {selectDefaultChannel} from 'app/actions/views/channel'; +import {showOverlay} from 'app/actions/navigation'; import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from 'app/actions/views/root'; -import {NavigationTypes} from 'app/constants'; +import {NavigationTypes, ViewTypes} from 'app/constants'; import {getTranslations} from 'app/i18n'; import mattermostManaged from 'app/mattermost_managed'; import PushNotifications from 'app/push_notifications'; @@ -38,12 +40,15 @@ class GlobalEventHandler { EventEmitter.on(General.SERVER_VERSION_CHANGED, this.onServerVersionChanged); EventEmitter.on(General.CONFIG_CHANGED, this.onServerConfigChanged); EventEmitter.on(General.SWITCH_TO_DEFAULT_CHANNEL, this.onSwitchToDefaultChannel); + this.turnOnInAppNotificationHandling(); Dimensions.addEventListener('change', this.onOrientationChange); AppState.addEventListener('change', this.onAppStateChange); Linking.addEventListener('url', this.onDeepLink); } appActive = async () => { + this.turnOnInAppNotificationHandling(); + // if the app is being controlled by an EMM provider if (emmProvider.enabled && emmProvider.inAppPinCode) { const authExpired = (Date.now() - emmProvider.inBackgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER; @@ -57,6 +62,8 @@ class GlobalEventHandler { }; appInactive = () => { + this.turnOffInAppNotificationHandling(); + const {dispatch} = this.store; // When the app is sent to the background we set the time when that happens @@ -68,7 +75,7 @@ class GlobalEventHandler { configure = (opts) => { this.store = opts.store; - this.launchEntry = opts.launchEntry; + this.launchApp = opts.launchApp; const window = Dimensions.get('window'); this.onOrientationChange({window}); @@ -139,8 +146,8 @@ class GlobalEventHandler { PushNotifications.setApplicationIconBadgeNumber(0); PushNotifications.cancelAllLocalNotifications(); // TODO: Only cancel the notification that belongs to this server - if (this.launchEntry) { - this.launchEntry(); + if (this.launchApp) { + this.launchApp(); } }; @@ -174,9 +181,8 @@ class GlobalEventHandler { ); } - if (this.launchEntry) { - const credentials = await getAppCredentials(); - this.launchEntry(credentials); + if (this.launchApp) { + this.launchApp(); } }; @@ -228,6 +234,31 @@ class GlobalEventHandler { dispatch(logout()); } }; + + turnOnInAppNotificationHandling = () => { + EventEmitter.on(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); + } + + turnOffInAppNotificationHandling = () => { + EventEmitter.off(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); + } + + handleInAppNotification = (notification) => { + const {data} = notification; + const {dispatch, getState} = this.store; + const state = getState(); + const currentChannelId = getCurrentChannelId(state); + + if (data && data.channel_id !== currentChannelId) { + const screen = 'Notification'; + const passProps = { + notification, + }; + + EventEmitter.emit(NavigationTypes.NAVIGATION_SHOW_OVERLAY); + dispatch(showOverlay(screen, passProps)); + } + }; } export default new GlobalEventHandler(); diff --git a/app/mattermost.js b/app/mattermost.js index 9ad9e2190..f9d160281 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -2,11 +2,12 @@ // See LICENSE.txt for license information. import {Linking, NativeModules, Platform} from 'react-native'; -import {Navigation, NativeEventsReceiver} from 'react-native-navigation'; +import {Navigation} from 'react-native-navigation'; import {Provider} from 'react-redux'; import {loadMe} from 'mattermost-redux/actions/users'; +import {resetToChannel, resetToSelectServer} from 'app/actions/navigation'; import {setDeepLinkURL} from 'app/actions/views/root'; import initialState from 'app/initial_state'; import {getAppCredentials} from 'app/init/credentials'; @@ -15,95 +16,58 @@ import 'app/init/fetch'; import globalEventHandler from 'app/init/global_event_handler'; import {registerScreens} from 'app/screens'; import configureStore from 'app/store'; -import ephemeralStore from 'app/store/ephemeral_store'; +import EphemeralStore from 'app/store/ephemeral_store'; import telemetry from 'app/telemetry'; import pushNotificationsUtils from 'app/utils/push_notifications'; const {MattermostShare} = NativeModules; -const startedSharedExtension = Platform.OS === 'android' && MattermostShare.isOpened; +const sharedExtensionStarted = Platform.OS === 'android' && MattermostShare.isOpened; export const store = configureStore(initialState); const init = async () => { - const credentials = await getAppCredentials(); + if (EphemeralStore.appStarted) { + launchApp(); + return; + } + pushNotificationsUtils.configure(store); globalEventHandler.configure({ store, - launchEntry, + launchApp, }); registerScreens(store, Provider); - if (startedSharedExtension) { - ephemeralStore.appStarted = true; + if (sharedExtensionStarted) { + EphemeralStore.appStarted = true; } - if (!ephemeralStore.appStarted) { - launchEntryAndAuthenticateIfNeeded(credentials); + if (!EphemeralStore.appStarted) { + launchAppAndAuthenticateIfNeeded(); } }; -const launchSelectServer = () => { - Navigation.startSingleScreenApp({ - screen: { - screen: 'SelectServer', - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - }, - }, - passProps: { - allowOtherServers: emmProvider.allowOtherServers, - }, - appStyle: { - orientation: 'auto', - }, - animationType: 'fade', - }); -}; - -const launchChannel = (skipMetrics = false) => { - Navigation.startSingleScreenApp({ - screen: { - screen: 'Channel', - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - }, - }, - passProps: { - skipMetrics, - }, - appStyle: { - orientation: 'auto', - }, - animationType: 'fade', - }); -}; - -const launchEntry = (credentials) => { +const launchApp = async () => { telemetry.start([ 'start:select_server_screen', 'start:channel_screen', ]); + const credentials = await getAppCredentials(); if (credentials) { store.dispatch(loadMe()); - launchChannel(); + store.dispatch(resetToChannel({skipMetrics: true})); } else { - launchSelectServer(); + store.dispatch(resetToSelectServer(emmProvider.allowOtherServers)); } telemetry.startSinceLaunch(['start:splash_screen']); - ephemeralStore.appStarted = true; + EphemeralStore.appStarted = true; }; -const launchEntryAndAuthenticateIfNeeded = async (credentials) => { +const launchAppAndAuthenticateIfNeeded = async () => { await emmProvider.handleManagedConfig(store); - launchEntry(credentials); + await launchApp(); if (emmProvider.enabled) { if (emmProvider.jailbreakProtection) { @@ -120,16 +84,14 @@ const launchEntryAndAuthenticateIfNeeded = async (credentials) => { }); }; -new NativeEventsReceiver().appLaunched(async () => { - const credentials = await getAppCredentials(); - if (startedSharedExtension) { - ephemeralStore.appStarted = true; - await launchEntryAndAuthenticateIfNeeded(credentials); - } else if (credentials) { - launchChannel(true); - } else { - launchSelectServer(); - } -}); +Navigation.events().registerAppLaunchedListener(() => { + init(); -init(); + // Keep track of the latest componentId to appear and disappear + Navigation.events().registerComponentDidAppearListener(({componentId}) => { + EphemeralStore.addComponentIdToStack(componentId); + }); + Navigation.events().registerComponentDidDisappearListener(({componentId}) => { + EphemeralStore.removeComponentIdFromStack(componentId); + }); +}); diff --git a/app/screens/about/about.js b/app/screens/about/about.js index 69aa156a4..2417768c1 100644 --- a/app/screens/about/about.js +++ b/app/screens/about/about.js @@ -23,15 +23,15 @@ const MATTERMOST_BUNDLE_IDS = ['com.mattermost.rnbeta', 'com.mattermost.rn']; export default class About extends PureComponent { static propTypes = { + componentId: PropTypes.string, config: PropTypes.object.isRequired, license: PropTypes.object.isRequired, - navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, }; componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } } diff --git a/app/screens/add_reaction/add_reaction.js b/app/screens/add_reaction/add_reaction.js index ff4183c2f..e03ddbf85 100644 --- a/app/screens/add_reaction/add_reaction.js +++ b/app/screens/add_reaction/add_reaction.js @@ -7,6 +7,7 @@ import { StyleSheet, View, } from 'react-native'; +import {Navigation} from 'react-native-navigation'; import EmojiPicker from 'app/components/emoji_picker'; import {emptyFunction} from 'app/utils/general'; @@ -14,8 +15,12 @@ import {setNavigatorStyles} from 'app/utils/theme'; export default class AddReaction extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + dismissModal: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, + }).isRequired, + componentId: PropTypes.string, closeButton: PropTypes.object, - navigator: PropTypes.object.isRequired, onEmojiPress: PropTypes.func, theme: PropTypes.object.isRequired, }; @@ -31,32 +36,29 @@ export default class AddReaction extends PureComponent { constructor(props) { super(props); - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - props.navigator.setButtons({ + props.actions.setButtons(props.componentId, { leftButtons: [{...this.leftButton, icon: props.closeButton}], }); } + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + } + componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); + } + } + + navigationButtonPressed({buttonId}) { + if (buttonId === 'close-edit-post') { + this.close(); } } close = () => { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); - }; - - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - switch (event.id) { - case 'close-edit-post': - this.close(); - break; - } - } + this.props.actions.dismissModal(); }; handleEmojiPress = (emoji) => { diff --git a/app/screens/add_reaction/index.js b/app/screens/add_reaction/index.js index 1c3d7556b..eb24346b2 100644 --- a/app/screens/add_reaction/index.js +++ b/app/screens/add_reaction/index.js @@ -1,10 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {dismissModal, setButtons} from 'app/actions/navigation'; + import AddReaction from './add_reaction'; function mapStateToProps(state) { @@ -13,4 +16,13 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(AddReaction); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + dismissModal, + setButtons, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(AddReaction); diff --git a/app/screens/channel/channel.android.js b/app/screens/channel/channel.android.js index 14748150e..866911888 100644 --- a/app/screens/channel/channel.android.js +++ b/app/screens/channel/channel.android.js @@ -20,35 +20,30 @@ import ChannelBase, {ClientUpgradeListener, style} from './channel_base'; export default class ChannelAndroid extends ChannelBase { render() { const {height} = Dimensions.get('window'); - const { - navigator, - } = this.props; const channelLoaderStyle = [style.channelLoader, {height}]; const drawerContent = ( - + - + - {LocalConfig.EnableMobileClientUpgrade && } + {LocalConfig.EnableMobileClientUpgrade && } ); diff --git a/app/screens/channel/channel.ios.js b/app/screens/channel/channel.ios.js index d53d567de..5e9189102 100644 --- a/app/screens/channel/channel.ios.js +++ b/app/screens/channel/channel.ios.js @@ -25,11 +25,19 @@ const CHANNEL_POST_TEXTBOX_CURSOR_CHANGE = 'onChannelTextBoxCursorChange'; const CHANNEL_POST_TEXTBOX_VALUE_CHANGE = 'onChannelTextBoxValueChange'; export default class ChannelIOS extends ChannelBase { + previewChannel = (passProps, options) => { + const {actions} = this.props; + const screen = 'ChannelPeek'; + + actions.peek(screen, passProps, options); + }; + + optionalProps = {previewChannel: this.previewChannel}; + render() { const {height} = Dimensions.get('window'); const { currentChannelId, - navigator, } = this.props; const channelLoaderStyle = [style.channelLoader, {height}]; @@ -39,17 +47,15 @@ export default class ChannelIOS extends ChannelBase { const drawerContent = ( - + @@ -65,7 +71,7 @@ export default class ChannelIOS extends ChannelBase { height={height} style={channelLoaderStyle} /> - {LocalConfig.EnableMobileClientUpgrade && } + {LocalConfig.EnableMobileClientUpgrade && } ); - return this.renderChannel(drawerContent); + return this.renderChannel(drawerContent, this.optionalProps); } } diff --git a/app/screens/channel/channel_base.js b/app/screens/channel/channel_base.js index f370f09f7..8476eb688 100644 --- a/app/screens/channel/channel_base.js +++ b/app/screens/channel/channel_base.js @@ -6,12 +6,12 @@ import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; import { Keyboard, - Platform, StyleSheet, View, } from 'react-native'; - +import {Navigation} from 'react-native-navigation'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; + import EventEmitter from 'mattermost-redux/utils/event_emitter'; import EmptyToolbar from 'app/components/start/empty_toolbar'; @@ -22,7 +22,7 @@ import SettingsSidebar from 'app/components/sidebars/settings'; import {preventDoubleTap} from 'app/utils/tap'; import PushNotifications from 'app/push_notifications'; -import ephemeralStore from 'app/store/ephemeral_store'; +import EphemeralStore from 'app/store/ephemeral_store'; import tracker from 'app/utils/time_tracker'; import telemetry from 'app/telemetry'; @@ -38,12 +38,15 @@ export default class ChannelBase extends PureComponent { selectDefaultTeam: PropTypes.func.isRequired, selectInitialChannel: PropTypes.func.isRequired, recordLoadTime: PropTypes.func.isRequired, + peek: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string.isRequired, currentChannelId: PropTypes.string, channelsRequestFailed: PropTypes.bool, currentTeamId: PropTypes.string, isLandscape: PropTypes.bool, - navigator: PropTypes.object, theme: PropTypes.object.isRequired, showTermsOfService: PropTypes.bool, disableTermsModal: PropTypes.bool, @@ -64,8 +67,10 @@ export default class ChannelBase extends PureComponent { this.postTextbox = React.createRef(); this.keyboardTracker = React.createRef(); - props.navigator.setStyle({ - screenBackgroundColor: props.theme.centerChannelBg, + Navigation.mergeOptions(props.componentId, { + layout: { + backgroundColor: props.theme.centerChannelBg, + }, }); if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) { @@ -105,8 +110,10 @@ export default class ChannelBase extends PureComponent { componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - this.props.navigator.setStyle({ - screenBackgroundColor: nextProps.theme.centerChannelBg, + Navigation.mergeOptions(this.props.componentId, { + layout: { + backgroundColor: nextProps.theme.centerChannelBg, + }, }); } @@ -162,53 +169,32 @@ export default class ChannelBase extends PureComponent { }; showTermsOfServiceModal = async () => { - const {navigator, theme} = this.props; + const {actions, theme} = this.props; const closeButton = await MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor); - navigator.showModal({ - screen: 'TermsOfService', - animationType: 'slide-up', - title: '', - backButtonTitle: '', - animated: true, - navigatorStyle: { - navBarTextColor: theme.centerChannelColor, - navBarBackgroundColor: theme.centerChannelBg, - navBarButtonColor: theme.buttonBg, - screenBackgroundColor: theme.centerChannelBg, - modalPresentationStyle: 'overCurrentContext', - }, - overrideBackPress: true, - passProps: { - closeButton, - }, - }); - }; - - goToChannelInfo = preventDoubleTap(() => { - const {intl} = this.context; - const {navigator, theme} = this.props; + const screen = 'TermsOfService'; + const passProps = { + closeButton, + }; const options = { - 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, + layout: { + backgroundColor: theme.centerChannelBg, }, }; + actions.showModalOverCurrentContext(screen, passProps, options); + }; + + goToChannelInfo = preventDoubleTap(() => { + const {actions} = this.props; + const {intl} = this.context; + const screen = 'ChannelInfo'; + const title = intl.formatMessage({id: 'mobile.routes.channelInfo', defaultMessage: 'Info'}); + Keyboard.dismiss(); - if (Platform.OS === 'android') { - navigator.showModal(options); - } else { - requestAnimationFrame(() => { - navigator.push(options); - }); - } + requestAnimationFrame(() => { + actions.goToScreen(screen, title); + }); }); handleAutoComplete = (value) => { @@ -231,8 +217,8 @@ export default class ChannelBase extends PureComponent { loadChannelsIfNecessary(teamId).then(() => { loadProfilesAndTeamMembersForDMSidebar(teamId); - if (ephemeralStore.appStartedFromPushNotification) { - ephemeralStore.appStartedFromPushNotification = false; + if (EphemeralStore.appStartedFromPushNotification) { + EphemeralStore.appStartedFromPushNotification = false; } else { selectInitialChannel(teamId); } @@ -261,12 +247,11 @@ export default class ChannelBase extends PureComponent { } }; - renderChannel(drawerContent) { + renderChannel(drawerContent, optionalProps = {}) { const { channelsRequestFailed, currentChannelId, isLandscape, - navigator, theme, } = this.props; @@ -283,7 +268,7 @@ export default class ChannelBase extends PureComponent { const Loading = require('app/components/channel_loader').default; return ( - + {drawerContent} diff --git a/app/screens/channel/channel_nav_bar/channel_nav_bar.js b/app/screens/channel/channel_nav_bar/channel_nav_bar.js index 6e0a84f40..1ba9afc54 100644 --- a/app/screens/channel/channel_nav_bar/channel_nav_bar.js +++ b/app/screens/channel/channel_nav_bar/channel_nav_bar.js @@ -25,7 +25,6 @@ const { export default class ChannelNavBar extends PureComponent { static propTypes = { isLandscape: PropTypes.bool.isRequired, - navigator: PropTypes.object.isRequired, openChannelDrawer: PropTypes.func.isRequired, openSettingsDrawer: PropTypes.func.isRequired, onPress: PropTypes.func.isRequired, @@ -57,7 +56,7 @@ export default class ChannelNavBar extends PureComponent { }; render() { - const {isLandscape, navigator, onPress, theme} = this.props; + const {isLandscape, onPress, theme} = this.props; const {openChannelDrawer, openSettingsDrawer} = this.props; const style = getStyleFromTheme(theme); const padding = {paddingHorizontal: 0}; @@ -97,7 +96,6 @@ export default class ChannelNavBar extends PureComponent { /> diff --git a/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js b/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js index bd256ea2e..554d14f55 100644 --- a/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js +++ b/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js @@ -19,16 +19,15 @@ export default class ChannelSearchButton extends PureComponent { clearSearch: PropTypes.func.isRequired, showSearchModal: PropTypes.func.isRequired, }).isRequired, - navigator: PropTypes.object, theme: PropTypes.object, }; handlePress = preventDoubleTap(async () => { - const {actions, navigator} = this.props; + const {actions} = this.props; Keyboard.dismiss(); await actions.clearSearch(); - await actions.showSearchModal(navigator); + await actions.showSearchModal(); }); render() { diff --git a/app/screens/channel/channel_nav_bar/channel_search_button/index.js b/app/screens/channel/channel_nav_bar/channel_search_button/index.js index 12e7f3d09..8bbb62cfb 100644 --- a/app/screens/channel/channel_nav_bar/channel_search_button/index.js +++ b/app/screens/channel/channel_nav_bar/channel_search_button/index.js @@ -6,7 +6,7 @@ import {connect} from 'react-redux'; import {clearSearch} from 'mattermost-redux/actions/search'; -import {showSearchModal} from 'app/actions/views/search'; +import {showSearchModal} from 'app/actions/navigation'; import ChannelSearchButton from './channel_search_button'; diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js index 26e2383a6..7cf52e4a1 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -33,13 +33,13 @@ export default class ChannelPostList extends PureComponent { selectPost: PropTypes.func.isRequired, recordLoadTime: PropTypes.func.isRequired, refreshChannelWithRetry: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, }).isRequired, channelId: PropTypes.string.isRequired, channelRefreshingFailed: PropTypes.bool, currentUserId: PropTypes.string, lastViewedAt: PropTypes.number, loadMorePostsVisible: PropTypes.bool.isRequired, - navigator: PropTypes.object, postIds: PropTypes.array, postVisibility: PropTypes.number, refreshing: PropTypes.bool.isRequired, @@ -105,36 +105,23 @@ export default class ChannelPostList extends PureComponent { goToThread = (post) => { telemetry.start(['post_list:thread']); - const {actions, channelId, navigator, theme} = this.props; + const {actions, channelId} = this.props; const rootId = (post.root_id || post.id); Keyboard.dismiss(); actions.loadThreadIfNecessary(rootId); actions.selectPost(rootId); - const options = { - screen: 'Thread', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelId, - rootId, - }, + const screen = 'Thread'; + const title = ''; + const passProps = { + channelId, + rootId, }; - if (Platform.OS === 'android') { - navigator.showModal(options); - } else { - requestAnimationFrame(() => { - navigator.push(options); - }); - } + requestAnimationFrame(() => { + actions.goToScreen(screen, title, passProps); + }); }; loadMorePostsTop = () => { @@ -181,7 +168,6 @@ export default class ChannelPostList extends PureComponent { return ( ); }; @@ -194,7 +180,6 @@ export default class ChannelPostList extends PureComponent { currentUserId, lastViewedAt, loadMorePostsVisible, - navigator, refreshing, theme, } = this.props; @@ -223,7 +208,6 @@ export default class ChannelPostList extends PureComponent { currentUserId={currentUserId} lastViewedAt={lastViewedAt} channelId={channelId} - navigator={navigator} renderFooter={this.renderFooter} refreshing={refreshing} scrollViewNativeID={channelId} @@ -234,7 +218,7 @@ export default class ChannelPostList extends PureComponent { return ( {component} - + ); diff --git a/app/screens/channel/channel_post_list/index.js b/app/screens/channel/channel_post_list/index.js index 2e34bdea1..dd5cd8368 100644 --- a/app/screens/channel/channel_post_list/index.js +++ b/app/screens/channel/channel_post_list/index.js @@ -10,6 +10,7 @@ import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels' import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {goToScreen} from 'app/actions/navigation'; import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel'; import {recordLoadTime} from 'app/actions/views/root'; @@ -42,6 +43,7 @@ function mapDispatchToProps(dispatch) { selectPost, recordLoadTime, refreshChannelWithRetry, + goToScreen, }, dispatch), }; } diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js index 40b00d318..e26fc7e13 100644 --- a/app/screens/channel/index.js +++ b/app/screens/channel/index.js @@ -19,6 +19,7 @@ import { import {connection} from 'app/actions/device'; import {recordLoadTime} from 'app/actions/views/root'; import {selectDefaultTeam} from 'app/actions/views/select_team'; +import {peek, goToScreen, showModalOverCurrentContext} from 'app/actions/navigation'; import {isLandscape} from 'app/selectors/device'; import Channel from './channel'; @@ -48,6 +49,9 @@ function mapDispatchToProps(dispatch) { recordLoadTime, startPeriodicStatusUpdates, stopPeriodicStatusUpdates, + peek, + goToScreen, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/screens/channel_add_members/channel_add_members.js b/app/screens/channel_add_members/channel_add_members.js index b7b0da221..95eb6010a 100644 --- a/app/screens/channel_add_members/channel_add_members.js +++ b/app/screens/channel_add_members/channel_add_members.js @@ -9,6 +9,7 @@ import { Platform, View, } from 'react-native'; +import {Navigation} from 'react-native-navigation'; import {debounce} from 'mattermost-redux/actions/helpers'; import {General} from 'mattermost-redux/constants'; @@ -32,13 +33,15 @@ export default class ChannelAddMembers extends PureComponent { getProfilesNotInChannel: PropTypes.func.isRequired, handleAddChannelMembers: PropTypes.func.isRequired, searchProfiles: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string, currentChannelId: PropTypes.string.isRequired, currentChannelGroupConstrained: PropTypes.bool, currentTeamId: PropTypes.string.isRequired, currentUserId: PropTypes.string.isRequired, profilesNotInChannel: PropTypes.array.isRequired, - navigator: PropTypes.object, theme: PropTypes.object.isRequired, }; @@ -66,19 +69,20 @@ export default class ChannelAddMembers extends PureComponent { }; this.addButton = { - disabled: true, + enalbed: false, id: 'add-members', - title: context.intl.formatMessage({id: 'integrations.add', defaultMessage: 'Add'}), + text: context.intl.formatMessage({id: 'integrations.add', defaultMessage: 'Add'}), showAsAction: 'always', }; - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - props.navigator.setButtons({ + props.actions.setButtons(props.componentId, { rightButtons: [this.addButton], }); } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + const {actions, currentTeamId} = this.props; actions.getTeamStats(currentTeamId); @@ -88,14 +92,20 @@ export default class ChannelAddMembers extends PureComponent { } componentDidUpdate(prevProps) { - const {navigator, theme} = this.props; + const {componentId, theme} = this.props; const {adding, selectedIds} = this.state; const enabled = Object.keys(selectedIds).length > 0 && !adding; this.enableAddOption(enabled); if (theme !== prevProps.theme) { - setNavigatorStyles(navigator, theme); + setNavigatorStyles(componentId, theme); + } + } + + navigationButtonPressed({buttonId}) { + if (buttonId === this.addButton.id) { + this.handleAddMembersPress(); } } @@ -104,12 +114,13 @@ export default class ChannelAddMembers extends PureComponent { }; close = () => { - this.props.navigator.pop({animated: true}); + this.props.actions.popTopScreen(); }; enableAddOption = (enabled) => { - this.props.navigator.setButtons({ - rightButtons: [{...this.addButton, disabled: !enabled}], + const {actions, componentId} = this.props; + actions.setButtons(componentId, { + rightButtons: [{...this.addButton, enabled}], }); }; @@ -188,14 +199,6 @@ export default class ChannelAddMembers extends PureComponent { }); }; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - if (event.id === this.addButton.id) { - this.handleAddMembersPress(); - } - } - }; - onSearch = (text) => { if (text) { this.setState({term: text}); diff --git a/app/screens/channel_add_members/channel_add_members.test.js b/app/screens/channel_add_members/channel_add_members.test.js index 700dd6483..0684d5685 100644 --- a/app/screens/channel_add_members/channel_add_members.test.js +++ b/app/screens/channel_add_members/channel_add_members.test.js @@ -16,17 +16,15 @@ describe('ChannelAddMembers', () => { getProfilesNotInChannel: jest.fn().mockResolvedValue({}), handleAddChannelMembers: jest.fn().mockResolvedValue({}), searchProfiles: jest.fn().mockResolvedValue({data: []}), + setButtons: jest.fn(), + popTopScreen: jest.fn(), }, currentChannelId: 'current_channel_id', currentTeamId: 'current_team_id', currentUserId: 'current_user_id', profilesNotInChannel: [], - navigator: { - pop: jest.fn(), - setButtons: jest.fn(), - setOnNavigatorEvent: jest.fn(), - }, theme: Preferences.THEMES.default, + componentId: 'component-id', }; test('should render without error and call functions on mount', () => { @@ -35,12 +33,10 @@ describe('ChannelAddMembers', () => { expect(baseProps.actions.getTeamStats).toBeCalledTimes(1); expect(baseProps.actions.getTeamStats).toBeCalledWith(baseProps.currentTeamId); - const button = {disabled: true, id: 'add-members', title: 'Add', showAsAction: 'always'}; - expect(baseProps.navigator.setButtons).toBeCalledTimes(2); - expect(baseProps.navigator.setButtons.mock.calls[0][0]).toEqual({rightButtons: [button]}); - expect(baseProps.navigator.setButtons.mock.calls[1][0]).toEqual({rightButtons: [button]}); - - expect(baseProps.navigator.setOnNavigatorEvent).toBeCalledTimes(1); + const button = {enabled: false, id: 'add-members', text: 'Add', showAsAction: 'always'}; + expect(baseProps.actions.setButtons).toBeCalledTimes(2); + expect(baseProps.actions.setButtons.mock.calls[0][0]).toEqual(baseProps.componentId, {rightButtons: [button]}); + expect(baseProps.actions.setButtons.mock.calls[1][0]).toEqual(baseProps.componentId, {rightButtons: [button]}); }); test('should match state on clearSearch', () => { @@ -52,12 +48,11 @@ describe('ChannelAddMembers', () => { wrapper.setState({term: '', searchResults: []}); }); - test('should call props.navigator on close', () => { + test('should call props.popTopScreen on close', () => { const wrapper = shallowWithIntl(); wrapper.instance().close(); - expect(baseProps.navigator.pop).toBeCalledTimes(1); - expect(baseProps.navigator.pop).toBeCalledWith({animated: true}); + expect(baseProps.actions.popTopScreen).toBeCalledTimes(1); }); test('should match state on onProfilesLoaded', () => { diff --git a/app/screens/channel_add_members/index.js b/app/screens/channel_add_members/index.js index f44704853..c2efa8625 100644 --- a/app/screens/channel_add_members/index.js +++ b/app/screens/channel_add_members/index.js @@ -11,6 +11,7 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId, getProfilesNotInCurrentChannel} from 'mattermost-redux/selectors/entities/users'; +import {setButtons, popTopScreen} from 'app/actions/navigation'; import {handleAddChannelMembers} from 'app/actions/views/channel_add_members'; import ChannelAddMembers from './channel_add_members'; @@ -35,6 +36,8 @@ function mapDispatchToProps(dispatch) { getProfilesNotInChannel, handleAddChannelMembers, searchProfiles, + setButtons, + popTopScreen, }, dispatch), }; } diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js index d2e892cc8..c2a07a787 100644 --- a/app/screens/channel_info/channel_info.js +++ b/app/screens/channel_info/channel_info.js @@ -42,14 +42,18 @@ export default class ChannelInfo extends PureComponent { selectPenultimateChannel: PropTypes.func.isRequired, handleSelectChannel: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }), + componentId: PropTypes.string, viewArchivedChannels: PropTypes.bool.isRequired, canDeleteChannel: PropTypes.bool.isRequired, currentChannel: PropTypes.object.isRequired, currentChannelCreatorName: PropTypes.string, currentChannelMemberCount: PropTypes.number, currentUserId: PropTypes.string, - navigator: PropTypes.object, status: PropTypes.string, theme: PropTypes.object.isRequired, isChannelMuted: PropTypes.bool.isRequired, @@ -82,7 +86,7 @@ export default class ChannelInfo extends PureComponent { componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } let isFavorite = this.state.isFavorite; @@ -104,95 +108,61 @@ export default class ChannelInfo extends PureComponent { } close = (redirect = true) => { + const {actions} = this.props; + if (redirect) { - this.props.actions.setChannelDisplayName(''); + actions.setChannelDisplayName(''); } if (Platform.OS === 'android') { - this.props.navigator.dismissModal({animated: true}); + actions.dismissModal(); } else { - this.props.navigator.pop({animated: true}); + actions.popTopScreen(); } }; goToChannelAddMembers = preventDoubleTap(() => { + const {actions} = this.props; const {intl} = this.context; - const {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, - }, - }); + const screen = 'ChannelAddMembers'; + const title = intl.formatMessage({id: 'channel_header.addMembers', defaultMessage: 'Add Members'}); + + actions.goToScreen(screen, title); }); goToChannelMembers = preventDoubleTap(() => { + const {actions, canManageUsers} = this.props; const {intl} = this.context; - const {canManageUsers, navigator, theme} = this.props; const id = canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers'); const defaultMessage = canManageUsers ? 'Manage Members' : 'View Members'; + const screen = 'ChannelMembers'; + const title = intl.formatMessage({id, defaultMessage}); - 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, - }, - }); + actions.goToScreen(screen, title); }); goToPinnedPosts = preventDoubleTap(() => { + const {actions, currentChannel} = this.props; const {formatMessage} = this.context.intl; - const {actions, currentChannel, navigator, theme} = this.props; const id = t('channel_header.pinnedPosts'); const defaultMessage = 'Pinned Posts'; + const screen = 'PinnedPosts'; + const title = formatMessage({id, defaultMessage}); + const passProps = { + currentChannelId: currentChannel.id, + }; - actions.clearPinnedPosts(currentChannel.id); - navigator.push({ - backButtonTitle: '', - screen: 'PinnedPosts', - title: formatMessage({id, defaultMessage}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - currentChannelId: currentChannel.id, - }, - }); + actions.goToScreen(screen, title, passProps); }); handleChannelEdit = preventDoubleTap(() => { + const {actions} = this.props; const {intl} = this.context; - const {navigator, theme} = this.props; const id = t('mobile.channel_info.edit'); const defaultMessage = 'Edit Channel'; + const screen = 'EditChannel'; + const title = intl.formatMessage({id, defaultMessage}); - navigator.push({ - backButtonTitle: '', - screen: 'EditChannel', - title: intl.formatMessage({id, defaultMessage}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }); + actions.goToScreen(screen, title); }); handleLeave = () => { @@ -336,30 +306,22 @@ export default class ChannelInfo extends PureComponent { }); showPermalinkView = (postId) => { - const {actions, navigator} = this.props; + const {actions} = this.props; + const screen = 'Permalink'; + const passProps = { + isPermalink: true, + onClose: this.handleClosePermalink, + }; + const options = { + layout: { + backgroundColor: changeOpacity('#000', 0.2), + }, + }; actions.selectFocusedPostId(postId); - if (!this.showingPermalink) { - const options = { - screen: 'Permalink', - animationType: 'none', - backButtonTitle: '', - overrideBackPress: true, - navigatorStyle: { - navBarHidden: true, - screenBackgroundColor: changeOpacity('#000', 0.2), - modalPresentationStyle: 'overCurrentContext', - }, - passProps: { - isPermalink: true, - onClose: this.handleClosePermalink, - }, - }; - - this.showingPermalink = true; - navigator.showModal(options); - } + this.showingPermalink = true; + actions.showModalOverCurrentContext(screen, passProps, options); }; renderViewOrManageMembersRow = () => { @@ -509,7 +471,6 @@ export default class ChannelInfo extends PureComponent { currentChannel, currentChannelCreatorName, currentChannelMemberCount, - navigator, status, theme, isBot, @@ -544,7 +505,6 @@ export default class ChannelInfo extends PureComponent { displayName={currentChannel.display_name} header={currentChannel.header} memberCount={currentChannelMemberCount} - navigator={navigator} onPermalinkPress={this.handlePermalinkPress} purpose={currentChannel.purpose} status={status} diff --git a/app/screens/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js index 0f10ef56e..fad941d7a 100644 --- a/app/screens/channel_info/channel_info_header.js +++ b/app/screens/channel_info/channel_info_header.js @@ -23,7 +23,6 @@ export default class ChannelInfoHeader extends React.PureComponent { memberCount: PropTypes.number, displayName: PropTypes.string.isRequired, header: PropTypes.string, - navigator: PropTypes.object.isRequired, onPermalinkPress: PropTypes.func, purpose: PropTypes.string, status: PropTypes.string, @@ -41,7 +40,6 @@ export default class ChannelInfoHeader extends React.PureComponent { displayName, header, memberCount, - navigator, onPermalinkPress, purpose, status, @@ -88,7 +86,6 @@ export default class ChannelInfoHeader extends React.PureComponent { defaultMessage='Purpose' /> 0 && !removing; this.enableRemoveOption(enabled); if (theme !== prevProps.theme) { - setNavigatorStyles(navigator, theme); + setNavigatorStyles(componentId, theme); + } + } + + navigationButtonPressed({buttonId}) { + if (buttonId === this.removeButton.id) { + this.handleRemoveMembersPress(); } } @@ -96,13 +106,14 @@ export default class ChannelMembers extends PureComponent { }; close = () => { - this.props.navigator.pop({animated: true}); + this.props.actions.popTopScreen(); }; enableRemoveOption = (enabled) => { - if (this.props.canManageUsers) { - this.props.navigator.setButtons({ - rightButtons: [{...this.removeButton, disabled: !enabled}], + const {actions, canManageUsers, componentId} = this.props; + if (canManageUsers) { + actions.setButtons(componentId, { + rightButtons: [{...this.removeButton, enabled}], }); } }; @@ -188,14 +199,6 @@ export default class ChannelMembers extends PureComponent { this.setState({loading: false, profiles: [...profiles, ...data]}); }; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - if (event.id === this.removeButton.id) { - this.handleRemoveMembersPress(); - } - } - }; - onSearch = (text) => { if (text) { this.setState({term: text}); diff --git a/app/screens/channel_members/channel_members.test.js b/app/screens/channel_members/channel_members.test.js index 16da3a7d5..4ad691310 100644 --- a/app/screens/channel_members/channel_members.test.js +++ b/app/screens/channel_members/channel_members.test.js @@ -10,11 +10,6 @@ import CustomList from 'app/components/custom_list'; import ChannelMembers from './channel_members'; describe('ChannelMembers', () => { - const navigator = { - setOnNavigatorEvent: jest.fn(), - setButtons: jest.fn(), - }; - const baseProps = { theme: Preferences.THEMES.default, currentUserId: 'current-user-id', @@ -24,8 +19,10 @@ describe('ChannelMembers', () => { getProfilesInChannel: jest.fn().mockImplementation(() => Promise.resolve()), handleRemoveChannelMembers: jest.fn(), searchProfiles: jest.fn(), + setButtons: jest.fn(), + popTopScreen: jest.fn(), }, - navigator, + componentId: 'component-id', }; test('should match snapshot', () => { diff --git a/app/screens/channel_members/index.js b/app/screens/channel_members/index.js index b37975c3f..5c6b54f50 100644 --- a/app/screens/channel_members/index.js +++ b/app/screens/channel_members/index.js @@ -4,12 +4,14 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {handleRemoveChannelMembers} from 'app/actions/views/channel_members'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentChannel, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels'; import {makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users'; import {getProfilesInChannel, searchProfiles} from 'mattermost-redux/actions/users'; +import {setButtons, popTopScreen} from 'app/actions/navigation'; +import {handleRemoveChannelMembers} from 'app/actions/views/channel_members'; + import ChannelMembers from './channel_members'; function makeMapStateToProps() { @@ -40,6 +42,8 @@ function mapDispatchToProps(dispatch) { getProfilesInChannel, handleRemoveChannelMembers, searchProfiles, + setButtons, + popTopScreen, }, dispatch), }; } diff --git a/app/screens/channel_peek/channel_peek.js b/app/screens/channel_peek/channel_peek.js index 3fe98c4be..f517ba967 100644 --- a/app/screens/channel_peek/channel_peek.js +++ b/app/screens/channel_peek/channel_peek.js @@ -4,6 +4,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {Platform, View} from 'react-native'; +import {Navigation} from 'react-native-navigation'; import {getLastPostIndex} from 'mattermost-redux/utils/post_list'; @@ -19,26 +20,27 @@ export default class ChannelPeek extends PureComponent { channelId: PropTypes.string.isRequired, currentUserId: PropTypes.string, lastViewedAt: PropTypes.number, - navigator: PropTypes.object, - postIds: PropTypes.array.isRequired, + postIds: PropTypes.array, theme: PropTypes.object.isRequired, }; static defaultProps = { - postIds: [], postVisibility: 15, }; constructor(props) { super(props); - this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); props.actions.loadPostsIfNecessaryWithRetry(props.channelId); this.state = { visiblePostIds: this.getVisiblePostIds(props), }; } + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + } + componentWillReceiveProps(nextProps) { const {postIds: nextPostIds} = nextProps; @@ -53,17 +55,15 @@ export default class ChannelPeek extends PureComponent { }); } - getVisiblePostIds = (props) => { - return props.postIds.slice(0, 15); - }; - - onNavigatorEvent = (event) => { - if (event.type === 'PreviewActionPress') { - if (event.id === 'action-mark-as-read') { - const {actions, channelId} = this.props; - actions.markChannelViewedAndRead(channelId); - } + navigationButtonPressed({buttonId}) { + if (buttonId === 'action-mark-as-read') { + const {actions, channelId} = this.props; + actions.markChannelViewedAndRead(channelId); } + } + + getVisiblePostIds = (props) => { + return props.postIds?.slice(0, 15) || []; }; render() { @@ -71,7 +71,6 @@ export default class ChannelPeek extends PureComponent { channelId, currentUserId, lastViewedAt, - navigator, theme, } = this.props; @@ -88,7 +87,6 @@ export default class ChannelPeek extends PureComponent { currentUserId={currentUserId} lastViewedAt={lastViewedAt} channelId={channelId} - navigator={navigator} /> ); diff --git a/app/screens/client_upgrade/client_upgrade.js b/app/screens/client_upgrade/client_upgrade.js index ac7d6608b..c0768a3a5 100644 --- a/app/screens/client_upgrade/client_upgrade.js +++ b/app/screens/client_upgrade/client_upgrade.js @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; +import {Navigation} from 'react-native-navigation'; import PropTypes from 'prop-types'; import { Alert, @@ -25,14 +26,16 @@ export default class ClientUpgrade extends PureComponent { actions: PropTypes.shape({ logError: PropTypes.func.isRequired, setLastUpgradeCheck: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string, currentVersion: PropTypes.string, closeAction: PropTypes.func, userCheckedForUpgrade: PropTypes.bool, downloadLink: PropTypes.string.isRequired, forceUpgrade: PropTypes.bool, latestVersion: PropTypes.string, - navigator: PropTypes.object, theme: PropTypes.object.isRequired, upgradeType: PropTypes.string, }; @@ -44,13 +47,14 @@ export default class ClientUpgrade extends PureComponent { constructor(props) { super(props); - this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); this.state = { upgradeType: UpgradeTypes.NO_UPGRADE, }; } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + if (this.props.userCheckedForUpgrade) { this.checkUpgrade(this.props); } @@ -58,7 +62,13 @@ export default class ClientUpgrade extends PureComponent { componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); + } + } + + navigationButtonPressed({buttonId}) { + if (buttonId === 'close-upgrade') { + this.props.actions.dismissModal(); } } @@ -84,12 +94,18 @@ export default class ClientUpgrade extends PureComponent { } handleClose = () => { - if (this.props.closeAction) { - this.props.closeAction(); - } else if (this.props.userCheckedForUpgrade) { - this.props.navigator.pop(); + const { + closeAction, + userCheckedForUpgrade, + actions, + } = this.props; + + if (closeAction) { + closeAction(); + } else if (userCheckedForUpgrade) { + actions.popTopScreen(); } else { - this.props.navigator.dismissModal(); + actions.dismissModal(); } }; @@ -117,16 +133,6 @@ export default class ClientUpgrade extends PureComponent { }); }; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - if (event.id === 'close-upgrade') { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); - } - } - }; - renderMustUpgrade() { const {theme} = this.props; const styles = getStyleFromTheme(theme); diff --git a/app/screens/client_upgrade/index.js b/app/screens/client_upgrade/index.js index b0984934c..14f67f586 100644 --- a/app/screens/client_upgrade/index.js +++ b/app/screens/client_upgrade/index.js @@ -5,6 +5,7 @@ import {connect} from 'react-redux'; import {logError} from 'mattermost-redux/actions/errors'; +import {popTopScreen, dismissModal} from 'app/actions/navigation'; import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; import getClientUpgrade from 'app/selectors/client_upgrade'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; @@ -29,6 +30,8 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ logError, setLastUpgradeCheck, + popTopScreen, + dismissModal, }, dispatch), }; } diff --git a/app/screens/code/code.js b/app/screens/code/code.js index 414933cd8..019b3346b 100644 --- a/app/screens/code/code.js +++ b/app/screens/code/code.js @@ -18,7 +18,10 @@ import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/ut export default class Code extends React.PureComponent { static propTypes = { - navigator: PropTypes.object.isRequired, + actions: PropTypes.shape({ + popTopScreen: PropTypes.func.isRequired, + }).isRequired, + componentId: PropTypes.string, theme: PropTypes.object.isRequired, content: PropTypes.string.isRequired, }; @@ -29,7 +32,7 @@ export default class Code extends React.PureComponent { componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } } @@ -38,7 +41,7 @@ export default class Code extends React.PureComponent { } handleAndroidBack = () => { - this.props.navigator.pop(); + this.props.actions.popTopScreen(); return true; }; diff --git a/app/screens/code/index.js b/app/screens/code/index.js index 4b0abdcd9..c833020be 100644 --- a/app/screens/code/index.js +++ b/app/screens/code/index.js @@ -1,10 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {popTopScreen} from 'app/actions/navigation'; + import Code from './code'; function mapStateToProps(state) { @@ -13,4 +16,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(Code); \ No newline at end of file +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + popTopScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Code); \ No newline at end of file diff --git a/app/screens/create_channel/create_channel.js b/app/screens/create_channel/create_channel.js index 14cc18317..1e5994e7c 100644 --- a/app/screens/create_channel/create_channel.js +++ b/app/screens/create_channel/create_channel.js @@ -8,6 +8,7 @@ import { Keyboard, InteractionManager, } from 'react-native'; +import {Navigation} from 'react-native-navigation'; import {General, RequestStatus} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; @@ -17,7 +18,7 @@ import {setNavigatorStyles} from 'app/utils/theme'; export default class CreateChannel extends PureComponent { static propTypes = { - navigator: PropTypes.object.isRequired, + componentId: PropTypes.string, theme: PropTypes.object.isRequired, deviceWidth: PropTypes.number.isRequired, deviceHeight: PropTypes.number.isRequired, @@ -26,6 +27,9 @@ export default class CreateChannel extends PureComponent { closeButton: PropTypes.object, actions: PropTypes.shape({ handleCreateChannel: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, }), }; @@ -43,7 +47,7 @@ export default class CreateChannel extends PureComponent { rightButton = { id: 'create-channel', - disabled: true, + enabled: false, showAsAction: 'always', }; @@ -58,7 +62,7 @@ export default class CreateChannel extends PureComponent { header: '', }; - this.rightButton.title = context.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'}); + this.rightButton.text = context.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'}); if (props.closeButton) { this.left = {...this.leftButton, icon: props.closeButton}; @@ -72,17 +76,17 @@ export default class CreateChannel extends PureComponent { buttons.leftButtons = [this.left]; } - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - props.navigator.setButtons(buttons); + props.actions.setButtons(props.componentId, buttons); } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); this.emitCanCreateChannel(false); } componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } const {createChannelRequest} = nextProps; @@ -109,38 +113,50 @@ export default class CreateChannel extends PureComponent { } } + navigationButtonPressed({buttonId}) { + switch (buttonId) { + case 'close-new-channel': + this.close(!this.props.closeButton); + break; + case 'create-channel': + this.onCreateChannel(); + break; + } + } + close = (goBack = false) => { + const {actions} = this.props; if (goBack) { - this.props.navigator.pop({animated: true}); + actions.popTopScreen(); } else { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); + actions.dismissModal(); } }; emitCanCreateChannel = (enabled) => { + const {actions, componentId} = this.props; const buttons = { - rightButtons: [{...this.rightButton, disabled: !enabled}], + rightButtons: [{...this.rightButton, enabled}], }; if (this.left) { buttons.leftButtons = [this.left]; } - this.props.navigator.setButtons(buttons); + actions.setButtons(componentId, buttons); }; emitCreating = (loading) => { + const {actions, componentId} = this.props; const buttons = { - rightButtons: [{...this.rightButton, disabled: loading}], + rightButtons: [{...this.rightButton, enabled: !loading}], }; if (this.left) { buttons.leftButtons = [this.left]; } - this.props.navigator.setButtons(buttons); + actions.setButtons(componentId, buttons); }; onCreateChannel = () => { @@ -149,19 +165,6 @@ export default class CreateChannel extends PureComponent { this.props.actions.handleCreateChannel(displayName, purpose, header, this.props.channelType); }; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - switch (event.id) { - case 'close-new-channel': - this.close(!this.props.closeButton); - break; - case 'create-channel': - this.onCreateChannel(); - break; - } - } - }; - onDisplayNameChange = (displayName) => { this.setState({displayName}); }; @@ -176,7 +179,6 @@ export default class CreateChannel extends PureComponent { render() { const { - navigator, theme, deviceWidth, deviceHeight, @@ -191,7 +193,6 @@ export default class CreateChannel extends PureComponent { return ( { const {channel: {type}} = this.props; const isDirect = type === General.DM_CHANNEL || type === General.GM_CHANNEL; @@ -147,23 +162,25 @@ export default class EditChannel extends PureComponent { this.props.actions.setChannelDisplayName(this.state.displayName); } - this.props.navigator.pop({animated: true}); + this.props.actions.popTopScreen(); }; emitCanUpdateChannel = (enabled) => { + const {actions, componentId} = this.props; const buttons = { - rightButtons: [{...this.rightButton, disabled: !enabled}], + rightButtons: [{...this.rightButton, enabled}], }; - this.props.navigator.setButtons(buttons); + actions.setButtons(componentId, buttons); }; emitUpdating = (loading) => { + const {actions, componentId} = this.props; const buttons = { - rightButtons: [{...this.rightButton, disabled: loading}], + rightButtons: [{...this.rightButton, enabled: !loading}], }; - this.props.navigator.setButtons(buttons); + actions.setButtons(componentId, buttons); }; validateDisplayName = (displayName) => { @@ -238,19 +255,6 @@ export default class EditChannel extends PureComponent { } }; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - switch (event.id) { - case 'close-edit-channel': - this.close(); - break; - case 'edit-channel': - this.onUpdateChannel(); - break; - } - } - }; - onDisplayNameChange = (displayName) => { this.setState({displayName}); }; @@ -276,7 +280,6 @@ export default class EditChannel extends PureComponent { purpose: oldPurpose, type, }, - navigator, theme, currentTeamUrl, deviceWidth, @@ -293,7 +296,6 @@ export default class EditChannel extends PureComponent { return ( { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); + this.props.actions.dismissModal(); }; emitCanEditPost = (enabled) => { - this.props.navigator.setButtons({ + const {actions, componentId} = this.props; + actions.setButtons(componentId, { leftButtons: [{...this.leftButton, icon: this.props.closeButton}], - rightButtons: [{...this.rightButton, disabled: !enabled}], + rightButtons: [{...this.rightButton, enabled}], }); }; emitEditing = (loading) => { - this.props.navigator.setButtons({ + const {actions, componentId} = this.props; + actions.setButtons(componentId, { leftButtons: [{...this.leftButton, icon: this.props.closeButton}], - rightButtons: [{...this.rightButton, disabled: loading}], + rightButtons: [{...this.rightButton, enabled: !loading}], }); }; @@ -121,19 +136,6 @@ export default class EditPost extends PureComponent { 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) { diff --git a/app/screens/edit_post/index.js b/app/screens/edit_post/index.js index 3d0f296d6..435fb3741 100644 --- a/app/screens/edit_post/index.js +++ b/app/screens/edit_post/index.js @@ -7,6 +7,8 @@ import {connect} from 'react-redux'; import {editPost} from 'mattermost-redux/actions/posts'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {setButtons, dismissModal} from 'app/actions/navigation'; + import {getDimensions} from 'app/selectors/device'; import EditPost from './edit_post'; @@ -26,6 +28,8 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ editPost, + setButtons, + dismissModal, }, dispatch), }; } diff --git a/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap b/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap index d97a63400..2830f2a22 100644 --- a/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap +++ b/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap @@ -26,47 +26,6 @@ exports[`edit_profile should match snapshot 1`] = ` } } maxFileSize={20971520} - navigator={ - Object { - "dismissModal": [MockFunction], - "push": [MockFunction], - "setButtons": [MockFunction] { - "calls": Array [ - Array [ - Object { - "rightButtons": Array [ - Object { - "disabled": true, - "id": "update-profile", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "setOnNavigatorEvent": [MockFunction] { - "calls": Array [ - Array [ - [Function], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } onShowFileSizeWarning={[Function]} onShowUnsupportedMimeTypeWarning={[Function]} removeProfileImage={[Function]} diff --git a/app/screens/edit_profile/edit_profile.js b/app/screens/edit_profile/edit_profile.js index 104ece5e2..0d975b44f 100644 --- a/app/screens/edit_profile/edit_profile.js +++ b/app/screens/edit_profile/edit_profile.js @@ -8,6 +8,7 @@ import {Alert, View} from 'react-native'; import RNFetchBlob from 'rn-fetch-blob'; import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; import {DocumentPickerUtil} from 'react-native-document-picker'; +import {Navigation} from 'react-native-navigation'; import {Client4} from 'mattermost-redux/client'; @@ -87,11 +88,14 @@ export default class EditProfile extends PureComponent { setProfileImageUri: PropTypes.func.isRequired, removeProfileImage: PropTypes.func.isRequired, updateUser: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string, currentUser: PropTypes.object.isRequired, firstNameDisabled: PropTypes.bool.isRequired, lastNameDisabled: PropTypes.bool.isRequired, - navigator: PropTypes.object.isRequired, nicknameDisabled: PropTypes.bool.isRequired, positionDisabled: PropTypes.bool.isRequired, theme: PropTypes.object.isRequired, @@ -104,7 +108,7 @@ export default class EditProfile extends PureComponent { rightButton = { id: 'update-profile', - disabled: true, + enabled: false, showAsAction: 'always', }; @@ -115,10 +119,9 @@ export default class EditProfile extends PureComponent { const buttons = { rightButtons: [this.rightButton], }; - this.rightButton.title = context.intl.formatMessage({id: t('mobile.account.settings.save'), defaultMessage: 'Save'}); + this.rightButton.text = context.intl.formatMessage({id: t('mobile.account.settings.save'), defaultMessage: 'Save'}); - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - props.navigator.setButtons(buttons); + props.actions.setButtons(props.componentId, buttons); this.state = { email, @@ -130,6 +133,21 @@ export default class EditProfile extends PureComponent { }; } + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + } + + navigationButtonPressed({buttonId}) { + switch (buttonId) { + case 'update-profile': + this.submitUser(); + break; + case 'close-settings': + this.close(); + break; + } + } + canUpdate = (updatedField) => { const {currentUser} = this.props; const keys = Object.keys(this.state); @@ -161,21 +179,21 @@ export default class EditProfile extends PureComponent { }; close = () => { - if (this.props.commandType === 'Push') { - this.props.navigator.pop(); + const {commandType, actions} = this.props; + if (commandType === 'Push') { + actions.popTopScreen(); } else { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); + actions.dismissModal(); } }; emitCanUpdateAccount = (enabled) => { + const {actions, componentId} = this.props; const buttons = { - rightButtons: [{...this.rightButton, disabled: !enabled}], + rightButtons: [{...this.rightButton, enabled}], }; - this.props.navigator.setButtons(buttons); + actions.setButtons(componentId, buttons); }; handleRequestError = (error) => { @@ -239,9 +257,7 @@ export default class EditProfile extends PureComponent { handleRemoveProfileImage = () => { this.setState({profileImageRemove: true}); this.emitCanUpdateAccount(true); - this.props.navigator.dismissModal({ - animationType: 'none', - }); + this.props.actions.dismissModal(); } uploadProfileImage = async () => { @@ -279,19 +295,6 @@ export default class EditProfile extends PureComponent { }); }; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - switch (event.id) { - case 'update-profile': - this.submitUser(); - break; - case 'close-settings': - this.close(); - break; - } - } - }; - onShowFileSizeWarning = (filename) => { const {formatMessage} = this.context.intl; const fileSizeWarning = formatMessage({ @@ -498,7 +501,6 @@ export default class EditProfile extends PureComponent { const { currentUser, theme, - navigator, } = this.props; const { @@ -519,7 +521,6 @@ export default class EditProfile extends PureComponent { canTakeVideo={false} canBrowseVideoLibrary={false} maxFileSize={MAX_SIZE} - navigator={navigator} wrapper={true} uploadFiles={this.handleUploadProfileImage} removeProfileImage={this.handleRemoveProfileImage} diff --git a/app/screens/edit_profile/edit_profile.test.js b/app/screens/edit_profile/edit_profile.test.js index 80de77d1f..71bd67850 100644 --- a/app/screens/edit_profile/edit_profile.test.js +++ b/app/screens/edit_profile/edit_profile.test.js @@ -17,17 +17,13 @@ jest.mock('app/utils/theme', () => { }); describe('edit_profile', () => { - const navigator = { - setOnNavigatorEvent: jest.fn(), - setButtons: jest.fn(), - dismissModal: jest.fn(), - push: jest.fn(), - }; - const actions = { updateUser: jest.fn(), setProfileImageUri: jest.fn(), removeProfileImage: jest.fn(), + popTopScreen: jest.fn(), + dismissModal: jest.fn(), + setButtons: jest.fn(), }; const baseProps = { @@ -37,7 +33,6 @@ describe('edit_profile', () => { nicknameDisabled: true, positionDisabled: true, theme: Preferences.THEMES.default, - navigator, currentUser: { first_name: 'Dwight', last_name: 'Schrute', @@ -47,6 +42,7 @@ describe('edit_profile', () => { position: 'position', }, commandType: 'ShowModal', + componentId: 'component-id', }; test('should match snapshot', async () => { @@ -58,15 +54,9 @@ describe('edit_profile', () => { }); test('should match state on handleRemoveProfileImage', () => { - const newNavigator = { - dismissModal: jest.fn(), - setOnNavigatorEvent: jest.fn(), - setButtons: jest.fn(), - }; const wrapper = shallow( , {context: {intl: {formatMessage: jest.fn()}}}, ); @@ -80,7 +70,6 @@ describe('edit_profile', () => { expect(instance.emitCanUpdateAccount).toHaveBeenCalledTimes(1); expect(instance.emitCanUpdateAccount).toBeCalledWith(true); - expect(newNavigator.dismissModal).toHaveBeenCalledTimes(1); - expect(newNavigator.dismissModal).toBeCalledWith({animationType: 'none'}); + expect(baseProps.actions.dismissModal).toHaveBeenCalledTimes(1); }); }); diff --git a/app/screens/edit_profile/index.js b/app/screens/edit_profile/index.js index ee29b73a3..21eb519c7 100644 --- a/app/screens/edit_profile/index.js +++ b/app/screens/edit_profile/index.js @@ -8,6 +8,7 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +import {popTopScreen, dismissModal, setButtons} from 'app/actions/navigation'; import {setProfileImageUri, removeProfileImage, updateUser} from 'app/actions/views/edit_profile'; import EditProfile from './edit_profile'; @@ -49,6 +50,9 @@ function mapDispatchToProps(dispatch) { setProfileImageUri, removeProfileImage, updateUser, + popTopScreen, + dismissModal, + setButtons, }, dispatch), }; } diff --git a/app/screens/error_teams_list/error_teams_list.js b/app/screens/error_teams_list/error_teams_list.js index 7d303751b..a972997cd 100644 --- a/app/screens/error_teams_list/error_teams_list.js +++ b/app/screens/error_teams_list/error_teams_list.js @@ -7,6 +7,7 @@ import { InteractionManager, View, } from 'react-native'; +import {Navigation} from 'react-native-navigation'; import FailedNetworkAction from 'app/components/failed_network_action'; import Loading from 'app/components/loading'; @@ -31,42 +32,42 @@ export default class ErrorTeamsList extends PureComponent { connection: PropTypes.func.isRequired, logout: PropTypes.func.isRequired, selectDefaultTeam: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, }).isRequired, - navigator: PropTypes.object, + componentId: PropTypes.string, theme: PropTypes.object, }; constructor(props) { super(props); - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); this.state = { loading: false, }; } + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + } + componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); + } + } + + navigationButtonPressed({buttonId}) { + const {logout} = this.props.actions; + if (buttonId === 'logout') { + InteractionManager.runAfterInteractions(logout); } } goToChannelView = () => { - const {navigator, theme} = this.props; - - navigator.resetTo({ - screen: 'Channel', - animated: false, - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - disableTermsModal: true, - }, - }); + const passProps = { + disableTermsModal: true, + }; + this.props.actions.resetToChannel(passProps); }; getUserInfo = async () => { @@ -83,15 +84,6 @@ export default class ErrorTeamsList extends PureComponent { } } - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - const {logout} = this.props.actions; - if (event.id === 'logout') { - InteractionManager.runAfterInteractions(logout); - } - } - }; - render() { const {theme} = this.props; const styles = getStyleSheet(theme); diff --git a/app/screens/error_teams_list/error_teams_list.test.js b/app/screens/error_teams_list/error_teams_list.test.js index dcf9af079..229ec1f42 100644 --- a/app/screens/error_teams_list/error_teams_list.test.js +++ b/app/screens/error_teams_list/error_teams_list.test.js @@ -10,10 +10,6 @@ import FailedNetworkAction from 'app/components/failed_network_action'; import ErrorTeamsList from './error_teams_list'; describe('ErrorTeamsList', () => { - const navigator = { - setOnNavigatorEvent: () => {}, // eslint-disable-line no-empty-function - }; - const loadMe = async () => { return { data: {}, @@ -26,9 +22,10 @@ describe('ErrorTeamsList', () => { connection: () => {}, // eslint-disable-line no-empty-function logout: () => {}, // eslint-disable-line no-empty-function selectDefaultTeam: () => {}, // eslint-disable-line no-empty-function + resetToChannel: jest.fn(), }, + componentId: 'component-id', theme: Preferences.THEMES.default, - navigator, }; test('should match snapshot', () => { @@ -43,6 +40,7 @@ describe('ErrorTeamsList', () => { const selectDefaultTeam = jest.fn(); const logout = jest.fn(); const actions = { + ...baseProps.actions, loadMe, logout, selectDefaultTeam, diff --git a/app/screens/error_teams_list/index.js b/app/screens/error_teams_list/index.js index 31424ed03..981e62733 100644 --- a/app/screens/error_teams_list/index.js +++ b/app/screens/error_teams_list/index.js @@ -8,6 +8,8 @@ import {logout, loadMe} from 'mattermost-redux/actions/users'; import {connection} from 'app/actions/device'; import {selectDefaultTeam} from 'app/actions/views/select_team'; +import {resetToChannel} from 'app/actions/navigation'; + import ErrorTeamsList from './error_teams_list.js'; function mapDispatchToProps(dispatch) { @@ -17,6 +19,7 @@ function mapDispatchToProps(dispatch) { selectDefaultTeam, connection, loadMe, + resetToChannel, }, dispatch), }; } diff --git a/app/screens/expanded_announcement_banner/expanded_announcement_banner.js b/app/screens/expanded_announcement_banner/expanded_announcement_banner.js index 363105a53..3fb7a31ae 100644 --- a/app/screens/expanded_announcement_banner/expanded_announcement_banner.js +++ b/app/screens/expanded_announcement_banner/expanded_announcement_banner.js @@ -16,15 +16,15 @@ export default class ExpandedAnnouncementBanner extends React.PureComponent { static propTypes = { actions: PropTypes.shape({ dismissBanner: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, }).isRequired, allowDismissal: PropTypes.bool.isRequired, bannerText: PropTypes.string.isRequired, - navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, } close = () => { - this.props.navigator.pop(); + this.props.actions.popTopScreen(); }; dismissBanner = () => { @@ -67,7 +67,6 @@ export default class ExpandedAnnouncementBanner extends React.PureComponent { { - const {actions, navigator, theme} = this.props; + const {actions} = this.props; const channelId = post.channel_id; const rootId = (post.root_id || post.id); + const screen = 'Thread'; + const title = ''; + const passProps = { + channelId, + rootId, + }; Keyboard.dismiss(); actions.loadThreadIfNecessary(rootId); actions.selectPost(rootId); - - const options = { - screen: 'Thread', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelId, - rootId, - }, - }; - - navigator.push(options); + actions.goToScreen(screen, title, passProps); }; handleClosePermalink = () => { @@ -99,25 +99,15 @@ export default class FlaggedPosts extends PureComponent { }; handleHashtagPress = async (hashtag) => { - const {actions, navigator} = this.props; + const {actions} = this.props; - await navigator.dismissModal(); + await actions.dismissModal(); - actions.showSearchModal(navigator, '#' + hashtag); + actions.showSearchModal('#' + hashtag); }; keyExtractor = (item) => item; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - if (event.id === 'close-settings') { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); - } - } - }; - previewPost = (post) => { Keyboard.dismiss(); @@ -167,7 +157,6 @@ export default class FlaggedPosts extends PureComponent { previewPost={this.previewPost} highlightPinnedOrFlagged={false} goToThread={this.goToThread} - navigator={this.props.navigator} onHashtagPress={this.handleHashtagPress} onPermalinkPress={this.handlePermalinkPress} managedConfig={mattermostManaged.getCachedConfig()} @@ -181,29 +170,24 @@ export default class FlaggedPosts extends PureComponent { }; showPermalinkView = (postId, isPermalink) => { - const {actions, navigator} = this.props; + const {actions} = this.props; actions.selectFocusedPostId(postId); if (!this.showingPermalink) { + const screen = 'Permalink'; + const passProps = { + isPermalink, + onClose: this.handleClosePermalink, + }; const options = { - screen: 'Permalink', - animationType: 'none', - backButtonTitle: '', - overrideBackPress: true, - navigatorStyle: { - navBarHidden: true, - screenBackgroundColor: changeOpacity('#000', 0.2), - modalPresentationStyle: 'overCurrentContext', - }, - passProps: { - isPermalink, - onClose: this.handleClosePermalink, + layout: { + backgroundColor: changeOpacity('#000', 0.2), }, }; this.showingPermalink = true; - navigator.showModal(options); + actions.showModalOverCurrentContext(screen, passProps, options); } }; diff --git a/app/screens/flagged_posts/index.js b/app/screens/flagged_posts/index.js index 16d3446f8..d48a92c54 100644 --- a/app/screens/flagged_posts/index.js +++ b/app/screens/flagged_posts/index.js @@ -9,8 +9,13 @@ import {clearSearch, getFlaggedPosts} from 'mattermost-redux/actions/search'; import {RequestStatus} from 'mattermost-redux/constants'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import { + dismissModal, + goToScreen, + showSearchModal, + showModalOverCurrentContext, +} from 'app/actions/navigation'; import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel'; -import {showSearchModal} from 'app/actions/views/search'; import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list'; import FlaggedPosts from './flagged_posts'; @@ -42,6 +47,9 @@ function mapDispatchToProps(dispatch) { selectFocusedPostId, selectPost, showSearchModal, + dismissModal, + goToScreen, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/screens/image_preview/image_preview.js b/app/screens/image_preview/image_preview.js index f9965cca3..80dc568a8 100644 --- a/app/screens/image_preview/image_preview.js +++ b/app/screens/image_preview/image_preview.js @@ -21,6 +21,7 @@ import LinearGradient from 'react-native-linear-gradient'; import {intlShape} from 'react-intl'; import Permissions from 'react-native-permissions'; import Gallery from 'react-native-image-gallery'; +import {Navigation} from 'react-native-navigation'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; @@ -43,13 +44,17 @@ const ANIM_CONFIG = {duration: 300}; export default class ImagePreview extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + dismissModal: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, + }).isRequired, + componentId: PropTypes.string.isRequired, canDownloadFiles: PropTypes.bool.isRequired, deviceHeight: PropTypes.number.isRequired, deviceWidth: PropTypes.number.isRequired, files: PropTypes.array, getItemMeasures: PropTypes.func.isRequired, index: PropTypes.number.isRequired, - navigator: PropTypes.object, origin: PropTypes.object, target: PropTypes.object, theme: PropTypes.object.isRequired, @@ -66,8 +71,10 @@ export default class ImagePreview extends PureComponent { constructor(props) { super(props); - props.navigator.setStyle({ - screenBackgroundColor: '#000', + Navigation.mergeOptions(props.componentId, { + layout: { + backgroundColor: '#000', + }, }); this.openAnim = new Animated.Value(0); @@ -103,12 +110,14 @@ export default class ImagePreview extends PureComponent { }; close = () => { - const {getItemMeasures, navigator} = this.props; + const {actions, getItemMeasures, componentId} = this.props; const {index} = this.state; this.setState({animating: true}); - navigator.setStyle({ - screenBackgroundColor: 'transparent', + Navigation.mergeOptions(componentId, { + layout: { + backgroundColor: 'transparent', + }, }); getItemMeasures(index, (origin) => { @@ -117,7 +126,7 @@ export default class ImagePreview extends PureComponent { } this.animateOpenAnimToValue(0, () => { - navigator.dismissModal({animationType: 'none'}); + actions.dismissModal(); }); }); }; @@ -177,7 +186,7 @@ export default class ImagePreview extends PureComponent { }; renderAttachmentDocument = (file) => { - const {canDownloadFiles, theme, navigator} = this.props; + const {canDownloadFiles, theme} = this.props; return ( @@ -190,7 +199,6 @@ export default class ImagePreview extends PureComponent { file={file} iconHeight={100} iconWidth={100} - navigator={navigator} theme={theme} wrapperHeight={200} wrapperWidth={200} @@ -441,6 +449,7 @@ export default class ImagePreview extends PureComponent { }; showDownloadOptionsIOS = async () => { + const {actions} = this.props; const {formatMessage} = this.context.intl; const file = this.getCurrentFile(); const items = []; @@ -504,30 +513,17 @@ export default class ImagePreview extends PureComponent { }); } - const options = { - title: file.caption, - items, - onCancelPress: () => this.setHeaderAndFooterVisible(true), - }; - if (items.length) { this.setHeaderAndFooterVisible(false); - this.props.navigator.showModal({ - screen: 'OptionsModal', - title: '', - animationType: 'none', - passProps: { - ...options, - }, - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - modalPresentationStyle: 'overCurrentContext', - }, - }); + const screen = 'OptionsModal'; + const passProps = { + title: file.caption, + items, + onCancelPress: () => this.setHeaderAndFooterVisible(true), + }; + + actions.showModalOverCurrentContext(screen, passProps); } }; diff --git a/app/screens/image_preview/image_preview.test.js b/app/screens/image_preview/image_preview.test.js index e4e31a0f6..922e26551 100644 --- a/app/screens/image_preview/image_preview.test.js +++ b/app/screens/image_preview/image_preview.test.js @@ -6,6 +6,7 @@ import {shallow} from 'enzyme'; import { TouchableOpacity, } from 'react-native'; +import {Navigation} from 'react-native-navigation'; import Preferences from 'mattermost-redux/constants/preferences'; @@ -18,6 +19,13 @@ jest.mock('react-native-doc-viewer', () => { OpenFile: jest.fn(), }; }); +jest.mock('react-native-navigation', () => ({ + Navigation: { + mergeOptions: jest.fn(), + }, +})); + +Navigation.mergeOptions = jest.fn(); describe('ImagePreview', () => { const baseProps = { @@ -30,10 +38,14 @@ describe('ImagePreview', () => { ], getItemMeasures: jest.fn(), index: 0, - navigator: {setStyle: jest.fn()}, origin: {}, target: {}, theme: Preferences.THEMES.default, + componentId: 'component-id', + actions: { + dismissModal: jest.fn(), + showModalOverCurrentContext: jest.fn(), + }, }; test('should match snapshot', () => { @@ -67,21 +79,26 @@ describe('ImagePreview', () => { expect(wrapper.state('index')).toEqual(1); }); - test('should match call getItemMeasures & navigator.setStyle on close', () => { + test('should match call getItemMeasures & Navigation.mergeOptions on close', () => { const getItemMeasures = jest.fn(); - const navigator = {setStyle: jest.fn()}; const wrapper = shallow( , {context: {intl: {formatMessage: jest.fn()}}}, ); wrapper.instance().close(); - expect(navigator.setStyle).toHaveBeenCalledTimes(2); - expect(navigator.setStyle).toBeCalledWith({screenBackgroundColor: 'transparent'}); + expect(Navigation.mergeOptions).toHaveBeenCalledTimes(2); + expect(Navigation.mergeOptions).toHaveBeenCalledWith( + baseProps.componentId, + { + layout: { + backgroundColor: 'transparent', + }, + }, + ); }); }); diff --git a/app/screens/image_preview/index.js b/app/screens/image_preview/index.js index 425a1df99..e72f70bff 100644 --- a/app/screens/image_preview/index.js +++ b/app/screens/image_preview/index.js @@ -1,12 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getDimensions} from 'app/selectors/device'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {canDownloadFilesOnMobile} from 'mattermost-redux/selectors/entities/general'; +import {dismissModal, showModalOverCurrentContext} from 'app/actions/navigation'; +import {getDimensions} from 'app/selectors/device'; + import ImagePreview from './image_preview'; function mapStateToProps(state) { @@ -17,4 +20,13 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(ImagePreview); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + dismissModal, + showModalOverCurrentContext, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ImagePreview); diff --git a/app/screens/index.js b/app/screens/index.js index fa83753fb..498e7171a 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -1,69 +1,79 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import React from 'react'; import {Navigation} from 'react-native-navigation'; import {gestureHandlerRootHOC} from 'react-native-gesture-handler'; import Channel from 'app/screens/channel'; +import Root from 'app/components/root'; import SelectServer from 'app/screens/select_server'; -import {wrapWithContextProvider} from 'app/utils/wrap_context_provider'; export function registerScreens(store, Provider) { - Navigation.registerComponent('About', () => wrapWithContextProvider(require('app/screens/about').default), store, Provider); - Navigation.registerComponent('AddReaction', () => wrapWithContextProvider(require('app/screens/add_reaction').default), store, Provider); - Navigation.registerComponent('AdvancedSettings', () => wrapWithContextProvider(require('app/screens/settings/advanced_settings').default), store, Provider); - Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel, false), store, Provider); - Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(require('app/screens/channel_add_members').default), store, Provider); - Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(require('app/screens/channel_info').default), store, Provider); - Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(require('app/screens/channel_members').default), store, Provider); - Navigation.registerComponent('ChannelPeek', () => wrapWithContextProvider(require('app/screens/channel_peek').default), store, Provider); - Navigation.registerComponent('ClientUpgrade', () => wrapWithContextProvider(require('app/screens/client_upgrade').default), store, Provider); - Navigation.registerComponent('ClockDisplay', () => wrapWithContextProvider(require('app/screens/clock_display').default), store, Provider); - Navigation.registerComponent('Code', () => wrapWithContextProvider(require('app/screens/code').default), store, Provider); - Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(require('app/screens/create_channel').default), store, Provider); - Navigation.registerComponent('DisplaySettings', () => wrapWithContextProvider(require('app/screens/settings/display_settings').default), store, Provider); - Navigation.registerComponent('EditChannel', () => wrapWithContextProvider(require('app/screens/edit_channel').default), store, Provider); - Navigation.registerComponent('EditPost', () => wrapWithContextProvider(require('app/screens/edit_post').default), store, Provider); - Navigation.registerComponent('EditProfile', () => wrapWithContextProvider(require('app/screens/edit_profile').default), store, Provider); - Navigation.registerComponent('ExpandedAnnouncementBanner', () => wrapWithContextProvider(require('app/screens/expanded_announcement_banner').default), store, Provider); - Navigation.registerComponent('FlaggedPosts', () => wrapWithContextProvider(require('app/screens/flagged_posts').default), store, Provider); - Navigation.registerComponent('ForgotPassword', () => wrapWithContextProvider(require('app/screens/forgot_password').default), store, Provider); - Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(require('app/screens/image_preview').default), store, Provider); - Navigation.registerComponent('InteractiveDialog', () => wrapWithContextProvider(require('app/screens/interactive_dialog').default), store, Provider); - Navigation.registerComponent('Login', () => wrapWithContextProvider(require('app/screens/login').default), store, Provider); - Navigation.registerComponent('LoginOptions', () => wrapWithContextProvider(require('app/screens/login_options').default), store, Provider); - Navigation.registerComponent('LongPost', () => wrapWithContextProvider(require('app/screens/long_post').default), store, Provider); - Navigation.registerComponent('MFA', () => wrapWithContextProvider(require('app/screens/mfa').default), store, Provider); - Navigation.registerComponent('MoreChannels', () => wrapWithContextProvider(require('app/screens/more_channels').default), store, Provider); - Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(require('app/screens/more_dms').default), store, Provider); - Navigation.registerComponent('Notification', () => wrapWithContextProvider(require('app/screens/notification').default), store, Provider); - Navigation.registerComponent('NotificationSettings', () => wrapWithContextProvider(require('app/screens/settings/notification_settings').default), store, Provider); - Navigation.registerComponent('NotificationSettingsAutoResponder', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_auto_responder').default), store, Provider); - Navigation.registerComponent('NotificationSettingsEmail', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_email').default), store, Provider); - Navigation.registerComponent('NotificationSettingsMentions', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_mentions').default), store, Provider); - Navigation.registerComponent('NotificationSettingsMentionsKeywords', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_mentions_keywords').default), store, Provider); - Navigation.registerComponent('NotificationSettingsMobile', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_mobile').default), store, Provider); - Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(require('app/screens/options_modal').default), store, Provider); - Navigation.registerComponent('Permalink', () => wrapWithContextProvider(require('app/screens/permalink').default), store, Provider); - Navigation.registerComponent('PinnedPosts', () => wrapWithContextProvider(require('app/screens/pinned_posts').default), store, Provider); - Navigation.registerComponent('PostOptions', () => gestureHandlerRootHOC(wrapWithContextProvider(require('app/screens/post_options').default)), store, Provider); - Navigation.registerComponent('ReactionList', () => gestureHandlerRootHOC(wrapWithContextProvider(require('app/screens/reaction_list').default)), store, Provider); - Navigation.registerComponent('RecentMentions', () => wrapWithContextProvider(require('app/screens/recent_mentions').default), store, Provider); - Navigation.registerComponent('Root', () => require('app/screens/root').default, store, Provider); - Navigation.registerComponent('Search', () => wrapWithContextProvider(require('app/screens/search').default), store, Provider); - Navigation.registerComponent('SelectorScreen', () => wrapWithContextProvider(require('app/screens/selector_screen').default), store, Provider); - Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider); - Navigation.registerComponent('SelectTeam', () => wrapWithContextProvider(require('app/screens/select_team').default), store, Provider); - Navigation.registerComponent('SelectTimezone', () => wrapWithContextProvider(require('app/screens/timezone/select_timezone').default), store, Provider); - Navigation.registerComponent('Settings', () => wrapWithContextProvider(require('app/screens/settings/general').default), store, Provider); - Navigation.registerComponent('SSO', () => wrapWithContextProvider(require('app/screens/sso').default), store, Provider); - Navigation.registerComponent('Table', () => wrapWithContextProvider(require('app/screens/table').default), store, Provider); - Navigation.registerComponent('TableImage', () => wrapWithContextProvider(require('app/screens/table_image').default), store, Provider); - Navigation.registerComponent('TermsOfService', () => wrapWithContextProvider(require('app/screens/terms_of_service').default), store, Provider); - Navigation.registerComponent('TextPreview', () => wrapWithContextProvider(require('app/screens/text_preview').default), store, Provider); - Navigation.registerComponent('ThemeSettings', () => wrapWithContextProvider(require('app/screens/theme').default), store, Provider); - Navigation.registerComponent('Thread', () => wrapWithContextProvider(require('app/screens/thread').default), store, Provider); - Navigation.registerComponent('TimezoneSettings', () => wrapWithContextProvider(require('app/screens/timezone').default), store, Provider); - Navigation.registerComponent('ErrorTeamsList', () => wrapWithContextProvider(require('app/screens/error_teams_list').default), store, Provider); - Navigation.registerComponent('UserProfile', () => wrapWithContextProvider(require('app/screens/user_profile').default), store, Provider); -} + // TODO consolidate this with app/utils/wrap_context_provider + const wrapper = (Comp) => (props) => ( // eslint-disable-line react/display-name + + + + + + ); + + Navigation.registerComponent('About', () => wrapper(require('app/screens/about').default), () => require('app/screens/about').default); + Navigation.registerComponent('AddReaction', () => wrapper(require('app/screens/add_reaction').default), () => require('app/screens/add_reaction').default); + Navigation.registerComponent('AdvancedSettings', () => wrapper(require('app/screens/settings/advanced_settings').default), () => require('app/screens/settings/advanced_settings').default); + Navigation.registerComponent('Channel', () => wrapper(Channel), () => Channel); + Navigation.registerComponent('ChannelAddMembers', () => wrapper(require('app/screens/channel_add_members').default), () => require('app/screens/channel_add_members').default); + Navigation.registerComponent('ChannelInfo', () => wrapper(require('app/screens/channel_info').default), () => require('app/screens/channel_info').default); + Navigation.registerComponent('ChannelMembers', () => wrapper(require('app/screens/channel_members').default), () => require('app/screens/channel_members').default); + Navigation.registerComponent('ChannelPeek', () => wrapper(require('app/screens/channel_peek').default), () => require('app/screens/channel_peek').default); + Navigation.registerComponent('ClientUpgrade', () => wrapper(require('app/screens/client_upgrade').default), () => require('app/screens/client_upgrade').default); + Navigation.registerComponent('ClockDisplay', () => wrapper(require('app/screens/clock_display').default), () => require('app/screens/clock_display').default); + Navigation.registerComponent('Code', () => wrapper(require('app/screens/code').default), () => require('app/screens/code').default); + Navigation.registerComponent('CreateChannel', () => wrapper(require('app/screens/create_channel').default), () => require('app/screens/create_channel').default); + Navigation.registerComponent('DisplaySettings', () => wrapper(require('app/screens/settings/display_settings').default), () => require('app/screens/settings/display_settings').default); + Navigation.registerComponent('EditChannel', () => wrapper(require('app/screens/edit_channel').default), () => require('app/screens/edit_channel').default); + Navigation.registerComponent('EditPost', () => wrapper(require('app/screens/edit_post').default), () => require('app/screens/edit_post').default); + Navigation.registerComponent('EditProfile', () => wrapper(require('app/screens/edit_profile').default), () => require('app/screens/edit_profile').default); + Navigation.registerComponent('ExpandedAnnouncementBanner', () => wrapper(require('app/screens/expanded_announcement_banner').default), () => require('app/screens/expanded_announcement_banner').default); + Navigation.registerComponent('FlaggedPosts', () => wrapper(require('app/screens/flagged_posts').default), () => require('app/screens/flagged_posts').default); + Navigation.registerComponent('ForgotPassword', () => wrapper(require('app/screens/forgot_password').default), () => require('app/screens/forgot_password').default); + Navigation.registerComponent('ImagePreview', () => wrapper(require('app/screens/image_preview').default), () => require('app/screens/image_preview').default); + Navigation.registerComponent('InteractiveDialog', () => wrapper(require('app/screens/interactive_dialog').default), () => require('app/screens/interactive_dialog').default); + Navigation.registerComponent('Login', () => wrapper(require('app/screens/login').default), () => require('app/screens/login').default); + Navigation.registerComponent('LoginOptions', () => wrapper(require('app/screens/login_options').default), () => require('app/screens/login_options').default); + Navigation.registerComponent('LongPost', () => wrapper(require('app/screens/long_post').default), () => require('app/screens/long_post').default); + Navigation.registerComponent('MFA', () => wrapper(require('app/screens/mfa').default), () => require('app/screens/mfa').default); + Navigation.registerComponent('MoreChannels', () => wrapper(require('app/screens/more_channels').default), () => require('app/screens/more_channels').default); + Navigation.registerComponent('MoreDirectMessages', () => wrapper(require('app/screens/more_dms').default), () => require('app/screens/more_dms').default); + Navigation.registerComponent('Notification', () => wrapper(require('app/screens/notification').default), () => require('app/screens/notification').default); + Navigation.registerComponent('NotificationSettings', () => wrapper(require('app/screens/settings/notification_settings').default), () => require('app/screens/settings/notification_settings').default); + Navigation.registerComponent('NotificationSettingsAutoResponder', () => wrapper(require('app/screens/settings/notification_settings_auto_responder').default), () => require('app/screens/settings/notification_settings_auto_responder').default); + Navigation.registerComponent('NotificationSettingsEmail', () => wrapper(require('app/screens/settings/notification_settings_email').default), () => require('app/screens/settings/notification_settings_email').default); + Navigation.registerComponent('NotificationSettingsMentions', () => wrapper(require('app/screens/settings/notification_settings_mentions').default), () => require('app/screens/settings/notification_settings_mentions').default); + Navigation.registerComponent('NotificationSettingsMentionsKeywords', () => wrapper(require('app/screens/settings/notification_settings_mentions_keywords').default), () => require('app/screens/settings/notification_settings_mentions_keywords').default); + Navigation.registerComponent('NotificationSettingsMobile', () => wrapper(require('app/screens/settings/notification_settings_mobile').default), () => require('app/screens/settings/notification_settings_mobile').default); + Navigation.registerComponent('OptionsModal', () => wrapper(require('app/screens/options_modal').default), () => require('app/screens/options_modal').default); + Navigation.registerComponent('Permalink', () => wrapper(require('app/screens/permalink').default), () => require('app/screens/permalink').default); + Navigation.registerComponent('PinnedPosts', () => wrapper(require('app/screens/pinned_posts').default), () => require('app/screens/pinned_posts').default); + Navigation.registerComponent('PostOptions', () => gestureHandlerRootHOC(wrapper(require('app/screens/post_options').default)), () => require('app/screens/post_options').default); + Navigation.registerComponent('ReactionList', () => gestureHandlerRootHOC(wrapper(require('app/screens/reaction_list').default)), () => require('app/screens/reaction_list').default); + Navigation.registerComponent('RecentMentions', () => wrapper(require('app/screens/recent_mentions').default), () => require('app/screens/recent_mentions').default); + Navigation.registerComponent('Root', () => wrapper(Root), () => Root); + Navigation.registerComponent('Search', () => wrapper(require('app/screens/search').default), () => require('app/screens/search').default); + Navigation.registerComponent('SelectorScreen', () => wrapper(require('app/screens/selector_screen').default), () => require('app/screens/selector_screen').default); + Navigation.registerComponent('SelectServer', () => wrapper(SelectServer), () => SelectServer); + Navigation.registerComponent('SelectTeam', () => wrapper(require('app/screens/select_team').default), () => require('app/screens/select_team').default); + Navigation.registerComponent('SelectTimezone', () => wrapper(require('app/screens/timezone/select_timezone').default), () => require('app/screens/timezone/select_timezone').default); + Navigation.registerComponent('Settings', () => wrapper(require('app/screens/settings/general').default), () => require('app/screens/settings/general').default); + Navigation.registerComponent('SSO', () => wrapper(require('app/screens/sso').default), () => require('app/screens/sso').default); + Navigation.registerComponent('Table', () => wrapper(require('app/screens/table').default), () => require('app/screens/table').default); + Navigation.registerComponent('TableImage', () => wrapper(require('app/screens/table_image').default), () => require('app/screens/table_image').default); + Navigation.registerComponent('TermsOfService', () => wrapper(require('app/screens/terms_of_service').default), () => require('app/screens/terms_of_service').default); + Navigation.registerComponent('TextPreview', () => wrapper(require('app/screens/text_preview').default), () => require('app/screens/text_preview').default); + Navigation.registerComponent('ThemeSettings', () => wrapper(require('app/screens/theme').default), () => require('app/screens/theme').default); + Navigation.registerComponent('Thread', () => wrapper(require('app/screens/thread').default), () => require('app/screens/thread').default); + Navigation.registerComponent('TimezoneSettings', () => wrapper(require('app/screens/timezone').default), () => require('app/screens/timezone').default); + Navigation.registerComponent('ErrorTeamsList', () => wrapper(require('app/screens/error_teams_list').default), () => require('app/screens/error_teams_list').default); + Navigation.registerComponent('UserProfile', () => wrapper(require('app/screens/user_profile').default), () => require('app/screens/user_profile').default); +} \ No newline at end of file diff --git a/app/screens/interactive_dialog/dialog_element.js b/app/screens/interactive_dialog/dialog_element.js index e675bcf78..5003c12ea 100644 --- a/app/screens/interactive_dialog/dialog_element.js +++ b/app/screens/interactive_dialog/dialog_element.js @@ -25,7 +25,6 @@ export default class DialogElement extends PureComponent { options: PropTypes.arrayOf(PropTypes.object), value: PropTypes.any, onChange: PropTypes.func, - navigator: PropTypes.object, theme: PropTypes.object, }; @@ -71,7 +70,6 @@ export default class DialogElement extends PureComponent { theme, dataSource, options, - navigator, } = this.props; let {maxLength} = this.props; @@ -128,7 +126,6 @@ export default class DialogElement extends PureComponent { placeholder={placeholder} showRequiredAsterisk={true} selected={this.state.selected} - navigator={navigator} roundedBorders={false} /> ); diff --git a/app/screens/interactive_dialog/index.js b/app/screens/interactive_dialog/index.js index e68b387ea..6170b7b6e 100644 --- a/app/screens/interactive_dialog/index.js +++ b/app/screens/interactive_dialog/index.js @@ -7,6 +7,8 @@ import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {submitInteractiveDialog} from 'mattermost-redux/actions/integrations'; +import {dismissModal} from 'app/actions/navigation'; + import InteractiveDialog from './interactive_dialog'; function mapStateToProps(state) { @@ -29,6 +31,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ submitInteractiveDialog, + dismissModal, }, dispatch), }; } diff --git a/app/screens/interactive_dialog/interactive_dialog.js b/app/screens/interactive_dialog/interactive_dialog.js index 8f181a89d..e4888ee7e 100644 --- a/app/screens/interactive_dialog/interactive_dialog.js +++ b/app/screens/interactive_dialog/interactive_dialog.js @@ -4,6 +4,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {ScrollView, View} from 'react-native'; +import {Navigation} from 'react-native-navigation'; import {checkDialogElementForError, checkIfErrorsMatchElements} from 'mattermost-redux/utils/integration_utils'; @@ -21,10 +22,10 @@ export default class InteractiveDialog extends PureComponent { elements: PropTypes.arrayOf(PropTypes.object).isRequired, notifyOnCancel: PropTypes.bool, state: PropTypes.string, - navigator: PropTypes.object, theme: PropTypes.object, actions: PropTypes.shape({ submitInteractiveDialog: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, }).isRequired, }; @@ -36,8 +37,6 @@ export default class InteractiveDialog extends PureComponent { constructor(props) { super(props); - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - const values = {}; props.elements.forEach((e) => { values[e.name] = e.default || null; @@ -50,19 +49,21 @@ export default class InteractiveDialog extends PureComponent { }; } - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - switch (event.id) { - case 'submit-dialog': - this.handleSubmit(); - break; - case 'close-dialog': - this.notifyOnCancelIfNeeded(); - this.handleHide(); - break; - } + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + } + + navigationButtonPressed({buttonId}) { + switch (buttonId) { + case 'submit-dialog': + this.handleSubmit(); + break; + case 'close-dialog': + this.notifyOnCancelIfNeeded(); + this.handleHide(); + break; } - }; + } handleSubmit = async () => { const {elements} = this.props; @@ -135,9 +136,7 @@ export default class InteractiveDialog extends PureComponent { } handleHide = () => { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); + this.props.actions.dismissModal(); } onChange = (name, value) => { @@ -146,7 +145,7 @@ export default class InteractiveDialog extends PureComponent { } render() { - const {elements, theme, navigator} = this.props; + const {elements, theme} = this.props; const style = getStyleFromTheme(theme); return ( @@ -171,7 +170,6 @@ export default class InteractiveDialog extends PureComponent { options={e.options} value={this.state.values[e.name]} onChange={this.onChange} - navigator={navigator} theme={theme} /> ); diff --git a/app/screens/interactive_dialog/interactive_dialog.test.js b/app/screens/interactive_dialog/interactive_dialog.test.js index 7f36358bb..9030af549 100644 --- a/app/screens/interactive_dialog/interactive_dialog.test.js +++ b/app/screens/interactive_dialog/interactive_dialog.test.js @@ -19,11 +19,9 @@ describe('InteractiveDialog', () => { theme: Preferences.THEMES.default, actions: { submitInteractiveDialog: jest.fn(), - }, - navigator: { - setOnNavigatorEvent: jest.fn(), dismissModal: jest.fn(), }, + componentId: 'component-id', }; test('should set default values', async () => { @@ -37,11 +35,9 @@ describe('InteractiveDialog', () => { }); test('should submit dialog', async () => { - const submitInteractiveDialog = jest.fn(); const wrapper = shallow( , ); @@ -53,16 +49,14 @@ describe('InteractiveDialog', () => { }; wrapper.instance().handleSubmit(); - expect(submitInteractiveDialog).toHaveBeenCalledTimes(1); - expect(submitInteractiveDialog).toHaveBeenCalledWith(dialog); + expect(baseProps.actions.submitInteractiveDialog).toHaveBeenCalledTimes(1); + expect(baseProps.actions.submitInteractiveDialog).toHaveBeenCalledWith(dialog); }); test('should submit dialog on cancel', async () => { - const submitInteractiveDialog = jest.fn(); const wrapper = shallow( , ); @@ -74,22 +68,20 @@ describe('InteractiveDialog', () => { cancelled: true, }; - wrapper.instance().onNavigatorEvent({type: 'NavBarButtonPress', id: 'close-dialog'}); - expect(submitInteractiveDialog).toHaveBeenCalledTimes(1); - expect(submitInteractiveDialog).toHaveBeenCalledWith(dialog); + wrapper.instance().navigationButtonPressed({buttonId: 'close-dialog'}); + expect(baseProps.actions.submitInteractiveDialog).toHaveBeenCalledTimes(1); + expect(baseProps.actions.submitInteractiveDialog).toHaveBeenCalledWith(dialog); }); test('should not submit dialog on cancel', async () => { - const submitInteractiveDialog = jest.fn(); const wrapper = shallow( , ); - wrapper.instance().onNavigatorEvent({type: 'NavBarButtonPress', id: 'close-dialog'}); - expect(submitInteractiveDialog).not.toHaveBeenCalled(); + wrapper.instance().navigationButtonPressed({buttonId: 'close-dialog'}); + expect(baseProps.actions.submitInteractiveDialog).not.toHaveBeenCalled(); }); }); diff --git a/app/screens/login/index.js b/app/screens/login/index.js index 3a51f5254..5ec265094 100644 --- a/app/screens/login/index.js +++ b/app/screens/login/index.js @@ -4,11 +4,12 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import LoginActions from 'app/actions/views/login'; +import {login} from 'mattermost-redux/actions/users'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {login} from 'mattermost-redux/actions/users'; +import {resetToChannel, goToScreen} from 'app/actions/navigation'; +import LoginActions from 'app/actions/views/login'; import Login from './login.js'; @@ -30,6 +31,8 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ ...LoginActions, login, + resetToChannel, + goToScreen, }, dispatch), }; } diff --git a/app/screens/login/login.js b/app/screens/login/login.js index f05979b5d..0157f5567 100644 --- a/app/screens/login/login.js +++ b/app/screens/login/login.js @@ -32,19 +32,20 @@ import telemetry from 'app/telemetry'; import {RequestStatus} from 'mattermost-redux/constants'; -const mfaExpectedErrors = ['mfa.validate_token.authenticate.app_error', 'ent.mfa.validate_token.authenticate.app_error']; +export const mfaExpectedErrors = ['mfa.validate_token.authenticate.app_error', 'ent.mfa.validate_token.authenticate.app_error']; export default class Login extends PureComponent { static propTypes = { - navigator: PropTypes.object, - theme: PropTypes.object, actions: PropTypes.shape({ handleLoginIdChanged: PropTypes.func.isRequired, handlePasswordChanged: PropTypes.func.isRequired, handleSuccessfulLogin: PropTypes.func.isRequired, scheduleExpiredNotification: PropTypes.func.isRequired, login: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, }).isRequired, + theme: PropTypes.object, config: PropTypes.object.isRequired, license: PropTypes.object.isRequired, loginId: PropTypes.string.isRequired, @@ -66,6 +67,7 @@ export default class Login extends PureComponent { componentDidMount() { Dimensions.addEventListener('change', this.orientationDidChange); + setMfaPreflightDone(false); } @@ -84,45 +86,20 @@ export default class Login extends PureComponent { goToChannel = () => { telemetry.remove(['start:overall']); - const {navigator} = this.props; tracker.initialLoad = Date.now(); this.scheduleSessionExpiredNotification(); - navigator.resetTo({ - screen: 'Channel', - title: '', - animated: false, - backButtonTitle: '', - navigatorStyle: { - animated: true, - animationType: 'fade', - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - }, - }); + this.props.actions.resetToChannel(); }; goToMfa = () => { + const {actions} = this.props; const {intl} = this.context; - const {navigator, theme} = this.props; + const screen = 'MFA'; + const title = intl.formatMessage({id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'}); - this.setState({isLoading: false}); - - 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, - }, - }); + actions.goToScreen(screen, title); }; blur = () => { @@ -309,20 +286,12 @@ export default class Login extends PureComponent { }; forgotPassword = () => { + const {actions} = this.props; const {intl} = this.context; - const {navigator, theme} = this.props; - navigator.push({ - screen: 'ForgotPassword', - title: intl.formatMessage({id: 'password_form.title', defaultMessage: 'Password Reset'}), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }); + const screen = 'ForgotPassword'; + const title = intl.formatMessage({id: 'password_form.title', defaultMessage: 'Password Reset'}); + + actions.goToScreen(screen, title); } render() { diff --git a/app/screens/login/login.test.js b/app/screens/login/login.test.js index e2ed4935a..3a3998d4f 100644 --- a/app/screens/login/login.test.js +++ b/app/screens/login/login.test.js @@ -3,10 +3,13 @@ import React from 'react'; +import {RequestStatus} from 'mattermost-redux/constants'; + import FormattedText from 'app/components/formatted_text'; import {shallowWithIntl} from 'test/intl-test-helper'; +import {mfaExpectedErrors} from 'app/screens/login/login'; import Login from './login'; describe('Login', () => { @@ -27,6 +30,8 @@ describe('Login', () => { handleSuccessfulLogin: jest.fn(), scheduleExpiredNotification: jest.fn(), login: jest.fn(), + resetToChannel: jest.fn(), + goToScreen: jest.fn(), }, }; @@ -72,4 +77,70 @@ describe('Login', () => { expect(wrapper.find(FormattedText).find({id: 'login.forgot'}).exists()).toBe(false); }); + + test('should send the user to the login screen after login', (done) => { + let props = { + ...baseProps, + loginRequest: { + status: RequestStatus.NOT_STARTED, + }, + }; + + props.actions.handleSuccessfulLogin.mockImplementation(() => Promise.resolve()); + props.actions.resetToChannel.mockImplementation(() => { + done(); + }); + + const wrapper = shallowWithIntl(); + + expect(props.actions.resetToChannel).not.toHaveBeenCalled(); + + props = { + ...props, + loginRequest: { + status: RequestStatus.STARTED, + }, + }; + wrapper.setProps(props); + + expect(props.actions.resetToChannel).not.toHaveBeenCalled(); + + props = { + ...props, + loginRequest: { + status: RequestStatus.SUCCESS, + }, + }; + wrapper.setProps(props); + + // This test times out if resetToChannel hasn't been called + }); + + test('should go to MFA screen when login response returns MFA error', () => { + const mfaError = { + error: { + server_error_id: mfaExpectedErrors[0], + }, + }; + + const wrapper = shallowWithIntl(); + wrapper.instance().checkLoginResponse(mfaError); + + expect(baseProps.actions.goToScreen). + toHaveBeenCalledWith( + 'MFA', + 'Multi-factor Authentication', + ); + }); + + test('should go to ForgotPassword screen when forgotPassword is called', () => { + const wrapper = shallowWithIntl(); + wrapper.instance().forgotPassword(); + + expect(baseProps.actions.goToScreen). + toHaveBeenCalledWith( + 'ForgotPassword', + 'Password Reset', + ); + }); }); diff --git a/app/screens/login_options/index.js b/app/screens/login_options/index.js index bb885e5bd..e29cd446a 100644 --- a/app/screens/login_options/index.js +++ b/app/screens/login_options/index.js @@ -1,11 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {goToScreen} from 'app/actions/navigation'; + import LoginOptions from './login_options'; function mapStateToProps(state) { @@ -18,4 +21,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(LoginOptions); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(LoginOptions); diff --git a/app/screens/login_options/login_options.js b/app/screens/login_options/login_options.js index 7ce99601c..9df26aa0f 100644 --- a/app/screens/login_options/login_options.js +++ b/app/screens/login_options/login_options.js @@ -3,7 +3,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {injectIntl, intlShape} from 'react-intl'; +import {intlShape} from 'react-intl'; import { Dimensions, Image, @@ -23,16 +23,20 @@ import LocalConfig from 'assets/config'; import gitlab from 'assets/images/gitlab.png'; import logo from 'assets/images/logo.png'; -class LoginOptions extends PureComponent { +export default class LoginOptions extends PureComponent { static propTypes = { - intl: intlShape.isRequired, - navigator: PropTypes.object, + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, config: PropTypes.object.isRequired, license: PropTypes.object.isRequired, - theme: PropTypes.object, }; - componentWillMount() { + static contextTypes = { + intl: intlShape.isRequired, + }; + + componentDidMount() { Dimensions.addEventListener('change', this.orientationDidChange); } @@ -41,38 +45,21 @@ class LoginOptions extends PureComponent { } goToLogin = preventDoubleTap(() => { - 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, - }, - }); + const {actions} = this.props; + const {intl} = this.context; + const screen = 'Login'; + const title = intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}); + + actions.goToScreen(screen, title); }); goToSSO = (ssoType) => { - const {intl, navigator, theme} = this.props; - navigator.push({ - screen: 'SSO', - title: intl.formatMessage({id: 'mobile.routes.sso', defaultMessage: 'Single Sign-On'}), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - ssoType, - }, - }); + const {actions} = this.props; + const {intl} = this.context; + const screen = 'SSO'; + const title = intl.formatMessage({id: 'mobile.routes.sso', defaultMessage: 'Single Sign-On'}); + + actions.goToScreen(screen, title, {ssoType}); }; orientationDidChange = () => { @@ -306,8 +293,6 @@ const style = StyleSheet.create({ flexDirection: 'column', justifyContent: 'center', paddingHorizontal: 15, - paddingVertical: 50, + flex: 1, }, }); - -export default injectIntl(LoginOptions); diff --git a/app/screens/long_post/index.js b/app/screens/long_post/index.js index be8dedc13..eb3a3b58f 100644 --- a/app/screens/long_post/index.js +++ b/app/screens/long_post/index.js @@ -9,6 +9,7 @@ import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; import {getPost, makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {dismissModal, goToScreen} from 'app/actions/navigation'; import {loadThreadIfNecessary} from 'app/actions/views/channel'; import LongPost from './long_post'; @@ -37,6 +38,8 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ loadThreadIfNecessary, selectPost, + dismissModal, + goToScreen, }, dispatch), }; } diff --git a/app/screens/long_post/long_post.js b/app/screens/long_post/long_post.js index c28f86fd1..d05937850 100644 --- a/app/screens/long_post/long_post.js +++ b/app/screens/long_post/long_post.js @@ -11,6 +11,7 @@ import { import {intlShape} from 'react-intl'; import * as Animatable from 'react-native-animatable'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; +import {Navigation} from 'react-native-navigation'; import FileAttachmentList from 'app/components/file_attachment_list'; import FormattedText from 'app/components/formatted_text'; @@ -43,6 +44,8 @@ export default class LongPost extends PureComponent { actions: PropTypes.shape({ loadThreadIfNecessary: PropTypes.func.isRequired, selectPost: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, }).isRequired, channelName: PropTypes.string, fileIds: PropTypes.array, @@ -50,7 +53,6 @@ export default class LongPost extends PureComponent { isPermalink: PropTypes.bool, inThreadView: PropTypes.bool, managedConfig: PropTypes.object, - navigator: PropTypes.object, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, postId: PropTypes.string.isRequired, @@ -65,44 +67,38 @@ export default class LongPost extends PureComponent { intl: intlShape.isRequired, }; - constructor(props) { - super(props); + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + } - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); + navigationButtonPressed({buttonId}) { + if (buttonId === 'backPress') { + this.handleClose(); + } } goToThread = preventDoubleTap((post) => { - const {actions, navigator, theme} = this.props; + const {actions} = this.props; const channelId = post.channel_id; const rootId = (post.root_id || post.id); + const screen = 'Thread'; + const title = ''; + const passProps = { + channelId, + rootId, + }; actions.loadThreadIfNecessary(rootId); actions.selectPost(rootId); - const options = { - screen: 'Thread', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelId, - rootId, - }, - }; - - navigator.push(options); + actions.goToScreen(screen, title, passProps); }); handleClose = () => { - const {navigator} = this.props; + const {actions} = this.props; if (this.refs.view) { this.refs.view.zoomOut().then(() => { - navigator.dismissModal({animationType: 'none'}); + actions.dismissModal(); }); } }; @@ -117,20 +113,9 @@ export default class LongPost extends PureComponent { } }; - onNavigatorEvent = (event) => { - switch (event.id) { - case 'backPress': - this.handleClose(); - break; - default: - break; - } - }; - renderFileAttachments(style) { const { fileIds, - navigator, postId, } = this.props; @@ -144,7 +129,6 @@ export default class LongPost extends PureComponent { onLongPress={emptyFunction} postId={postId} toggleSelected={emptyFunction} - navigator={navigator} /> ); @@ -153,7 +137,7 @@ export default class LongPost extends PureComponent { } renderReactions = (style) => { - const {hasReactions, navigator, postId} = this.props; + const {hasReactions, postId} = this.props; if (!hasReactions) { return null; @@ -162,7 +146,6 @@ export default class LongPost extends PureComponent { return ( @@ -176,7 +159,6 @@ export default class LongPost extends PureComponent { fileIds, hasReactions, managedConfig, - navigator, onHashtagPress, onPermalinkPress, postId, @@ -241,7 +223,6 @@ export default class LongPost extends PureComponent { showLongPost={true} onHashtagPress={onHashtagPress} onPermalinkPress={onPermalinkPress} - navigator={navigator} managedConfig={managedConfig} /> diff --git a/app/screens/mfa/index.js b/app/screens/mfa/index.js index 4707c0bd2..4dea96eb2 100644 --- a/app/screens/mfa/index.js +++ b/app/screens/mfa/index.js @@ -6,6 +6,8 @@ import {connect} from 'react-redux'; import {login} from 'mattermost-redux/actions/users'; +import {popTopScreen} from 'app/actions/navigation'; + import Mfa from './mfa'; function mapStateToProps(state) { @@ -22,6 +24,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ login, + popTopScreen, }, dispatch), }; } diff --git a/app/screens/mfa/mfa.js b/app/screens/mfa/mfa.js index 1d7c64b70..f530f783b 100644 --- a/app/screens/mfa/mfa.js +++ b/app/screens/mfa/mfa.js @@ -28,9 +28,9 @@ import {setMfaPreflightDone} from 'app/utils/security'; export default class Mfa extends PureComponent { static propTypes = { - navigator: PropTypes.object, actions: PropTypes.shape({ login: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, }).isRequired, loginId: PropTypes.string.isRequired, password: PropTypes.string.isRequired, @@ -56,7 +56,7 @@ export default class Mfa extends PureComponent { // In case the login is successful the previous scene (login) will take care of the transition if (this.props.loginRequest.status === RequestStatus.STARTED && nextProps.loginRequest.status === RequestStatus.FAILURE) { - this.props.navigator.pop({animated: true}); + this.props.actions.popTopScreen(); } } diff --git a/app/screens/more_channels/index.js b/app/screens/more_channels/index.js index 679744979..fa8617a0b 100644 --- a/app/screens/more_channels/index.js +++ b/app/screens/more_channels/index.js @@ -12,11 +12,12 @@ import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/ import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {showCreateOption} from 'mattermost-redux/utils/channel_utils'; import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils'; - -import {handleSelectChannel, setChannelDisplayName} from 'app/actions/views/channel'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {setButtons, dismissModal, goToScreen} from 'app/actions/navigation'; +import {handleSelectChannel, setChannelDisplayName} from 'app/actions/views/channel'; + import MoreChannels from './more_channels'; const joinableChannels = createSelector( @@ -53,6 +54,9 @@ function mapDispatchToProps(dispatch) { getChannels, searchChannels, setChannelDisplayName, + setButtons, + dismissModal, + goToScreen, }, dispatch), }; } diff --git a/app/screens/more_channels/more_channels.js b/app/screens/more_channels/more_channels.js index 1d9a79a1f..416a0bf53 100644 --- a/app/screens/more_channels/more_channels.js +++ b/app/screens/more_channels/more_channels.js @@ -5,6 +5,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; import {Platform, View} from 'react-native'; +import {Navigation} from 'react-native-navigation'; import {debounce} from 'mattermost-redux/actions/helpers'; import {General} from 'mattermost-redux/constants'; @@ -28,13 +29,16 @@ export default class MoreChannels extends PureComponent { getChannels: PropTypes.func.isRequired, searchChannels: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string, canCreateChannels: PropTypes.bool.isRequired, channels: PropTypes.array, closeButton: PropTypes.object, currentUserId: PropTypes.string.isRequired, currentTeamId: PropTypes.string.isRequired, - navigator: PropTypes.object, theme: PropTypes.object.isRequired, }; @@ -52,6 +56,7 @@ export default class MoreChannels extends PureComponent { this.searchTimeoutId = 0; this.page = -1; this.next = true; + this.mounted = false; this.state = { channels: props.channels.slice(0, General.CHANNELS_CHUNK_SIZE), @@ -62,7 +67,7 @@ export default class MoreChannels extends PureComponent { this.rightButton = { id: 'create-pub-channel', - title: context.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'}), + text: context.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'}), showAsAction: 'always', }; @@ -79,20 +84,25 @@ export default class MoreChannels extends PureComponent { buttons.rightButtons = [this.rightButton]; } - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - props.navigator.setButtons(buttons); + props.actions.setButtons(props.componentId, buttons); } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + this.mounted = true; this.doGetChannels(); } + componentWillUnmount() { + this.mounted = false; + } + componentWillReceiveProps(nextProps) { const {term} = this.state; let channels; if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } if (nextProps.channels !== this.props.channels) { @@ -107,6 +117,17 @@ export default class MoreChannels extends PureComponent { } } + navigationButtonPressed({buttonId}) { + switch (buttonId) { + case 'close-more-channels': + this.close(); + break; + case 'create-pub-channel': + this.onCreateChannel(); + break; + } + } + cancelSearch = () => { const {channels} = this.props; @@ -117,14 +138,14 @@ export default class MoreChannels extends PureComponent { }; close = () => { - this.props.navigator.dismissModal({animationType: 'slide-down'}); + this.props.actions.dismissModal(); }; doGetChannels = () => { const {actions, currentTeamId} = this.props; const {loading, term} = this.state; - if (this.next && !loading && !term) { + if (this.next && !loading && !term && this.mounted) { this.setState({loading: true}, () => { actions.getChannels( currentTeamId, @@ -145,37 +166,26 @@ export default class MoreChannels extends PureComponent { getChannels = debounce(this.doGetChannels, 100); headerButtons = (createEnabled) => { - const {canCreateChannels} = this.props; + const {actions, canCreateChannels, componentId} = this.props; const buttons = { leftButtons: [this.leftButton], }; if (canCreateChannels) { - buttons.rightButtons = [{...this.rightButton, disabled: !createEnabled}]; + buttons.rightButtons = [{...this.rightButton, enabled: createEnabled}]; } - this.props.navigator.setButtons(buttons); + actions.setButtons(componentId, buttons); }; loadedChannels = ({data}) => { - if (data && !data.length) { - this.next = false; - } - - this.page += 1; - this.setState({loading: false}); - }; - - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - switch (event.id) { - case 'close-more-channels': - this.close(); - break; - case 'create-pub-channel': - this.onCreateChannel(); - break; + if (this.mounted) { + if (data && !data.length) { + this.next = false; } + + this.page += 1; + this.setState({loading: false}); } }; @@ -220,25 +230,16 @@ export default class MoreChannels extends PureComponent { }; onCreateChannel = () => { + const {actions} = this.props; const {formatMessage} = this.context.intl; - const {navigator, theme} = this.props; - navigator.push({ - screen: 'CreateChannel', - animationType: 'slide-up', - title: formatMessage({id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'}), - backButtonTitle: '', - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelType: General.OPEN_CHANNEL, - }, - }); + const screen = 'CreateChannel'; + const title = formatMessage({id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'}); + const passProps = { + channelType: General.OPEN_CHANNEL, + }; + + actions.goToScreen(screen, title, passProps); }; renderLoading = () => { diff --git a/app/screens/more_channels/more_channels.test.js b/app/screens/more_channels/more_channels.test.js index 51787ffcc..7fe062b9f 100644 --- a/app/screens/more_channels/more_channels.test.js +++ b/app/screens/more_channels/more_channels.test.js @@ -11,19 +11,15 @@ import MoreChannels from './more_channels.js'; jest.mock('react-intl'); describe('MoreChannels', () => { - const navigator = { - setOnNavigatorEvent: jest.fn(), - setButtons: jest.fn(), - dismissModal: jest.fn(), - push: jest.fn(), - }; - const actions = { handleSelectChannel: jest.fn(), joinChannel: jest.fn(), getChannels: jest.fn().mockResolvedValue({data: [{id: 'id2', name: 'name2', display_name: 'display_name2'}]}), searchChannels: jest.fn(), setChannelDisplayName: jest.fn(), + setButtons: jest.fn(), + dismissModal: jest.fn(), + goToScreen: jest.fn(), }; const baseProps = { @@ -33,8 +29,8 @@ describe('MoreChannels', () => { closeButton: {}, currentUserId: 'current_user_id', currentTeamId: 'current_team_id', - navigator, theme: Preferences.THEMES.default, + componentId: 'component-id', }; test('should match snapshot', () => { @@ -46,27 +42,25 @@ describe('MoreChannels', () => { expect(wrapper.getElement()).toMatchSnapshot(); }); - test('should call props.navigator.dismissModal on close', () => { + test('should call props.actions.dismissModal on close', () => { const wrapper = shallow( , {context: {intl: {formatMessage: jest.fn()}}}, ); wrapper.instance().close(); - expect(baseProps.navigator.dismissModal).toHaveBeenCalledTimes(1); - expect(baseProps.navigator.dismissModal).toHaveBeenCalledWith({animationType: 'slide-down'}); + expect(baseProps.actions.dismissModal).toHaveBeenCalledTimes(1); }); - test('should call props.navigator.setButtons on headerButtons', () => { - const props = {...baseProps, navigator: {...navigator, setButtons: jest.fn()}}; + test('should call props.actions.setButtons on headerButtons', () => { const wrapper = shallow( - , + , {context: {intl: {formatMessage: jest.fn()}}}, ); - expect(props.navigator.setButtons).toHaveBeenCalledTimes(1); + expect(baseProps.actions.setButtons).toHaveBeenCalledTimes(1); wrapper.instance().headerButtons(true); - expect(props.navigator.setButtons).toHaveBeenCalledTimes(2); + expect(baseProps.actions.setButtons).toHaveBeenCalledTimes(2); }); test('should match return value of filterChannels', () => { diff --git a/app/screens/more_dms/index.js b/app/screens/more_dms/index.js index 3427eaff8..0f92b9b0a 100644 --- a/app/screens/more_dms/index.js +++ b/app/screens/more_dms/index.js @@ -14,6 +14,8 @@ import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selector import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId, getUsers} from 'mattermost-redux/selectors/entities/users'; +import {dismissModal, setButtons} from 'app/actions/navigation'; + import MoreDirectMessages from './more_dms'; function mapStateToProps(state) { @@ -40,6 +42,8 @@ function mapDispatchToProps(dispatch) { getProfilesInTeam, searchProfiles, setChannelDisplayName, + dismissModal, + setButtons, }, dispatch), }; } diff --git a/app/screens/more_dms/more_dms.js b/app/screens/more_dms/more_dms.js index f16f5e99a..3f1640020 100644 --- a/app/screens/more_dms/more_dms.js +++ b/app/screens/more_dms/more_dms.js @@ -5,6 +5,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; import {Platform, View} from 'react-native'; +import {Navigation} from 'react-native-navigation'; import {debounce} from 'mattermost-redux/actions/helpers'; import {General} from 'mattermost-redux/constants'; @@ -38,12 +39,14 @@ export default class MoreDirectMessages extends PureComponent { getProfilesInTeam: PropTypes.func.isRequired, searchProfiles: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string, allProfiles: PropTypes.object.isRequired, currentDisplayName: PropTypes.string, currentTeamId: PropTypes.string.isRequired, currentUserId: PropTypes.string.isRequired, - navigator: PropTypes.object, restrictDirectMessage: PropTypes.bool.isRequired, teammateNameDisplay: PropTypes.string, theme: PropTypes.object.isRequired, @@ -59,6 +62,7 @@ export default class MoreDirectMessages extends PureComponent { this.searchTimeoutId = 0; this.next = true; this.page = -1; + this.mounted = false; this.state = { profiles: [], @@ -70,28 +74,42 @@ export default class MoreDirectMessages extends PureComponent { selectedCount: 0, }; - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); this.updateNavigationButtons(false, context); } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + this.mounted = true; + this.getProfiles(); } + componentWillUnmount() { + this.mounted = false; + } + componentDidUpdate(prevProps) { - const {navigator, theme} = this.props; + const {componentId, theme} = this.props; const {selectedCount, startingConversation} = this.state; const canStart = selectedCount > 0 && !startingConversation; this.updateNavigationButtons(canStart); if (theme !== prevProps.theme) { - setNavigatorStyles(navigator, theme); + setNavigatorStyles(componentId, theme); + } + } + + navigationButtonPressed({buttonId}) { + if (buttonId === START_BUTTON) { + this.startConversation(); + } else if (buttonId === CLOSE_BUTTON) { + this.close(); } } close = () => { - this.props.navigator.dismissModal({animationType: 'slide-down'}); + this.props.actions.dismissModal(); }; clearSearch = () => { @@ -100,7 +118,7 @@ export default class MoreDirectMessages extends PureComponent { getProfiles = debounce(() => { const {loading, term} = this.state; - if (this.next && !loading && !term) { + if (this.next && !loading && !term && this.mounted) { this.setState({loading: true}, () => { const {actions, currentTeamId, restrictDirectMessage} = this.props; @@ -171,13 +189,15 @@ export default class MoreDirectMessages extends PureComponent { }; loadedProfiles = ({data}) => { - const {profiles} = this.state; - if (data && !data.length) { - this.next = false; - } + if (this.mounted) { + const {profiles} = this.state; + if (data && !data.length) { + this.next = false; + } - this.page += 1; - this.setState({loading: false, profiles: [...profiles, ...data]}); + this.page += 1; + this.setState({loading: false, profiles: [...profiles, ...data]}); + } }; makeDirectChannel = async (id) => { @@ -235,16 +255,6 @@ export default class MoreDirectMessages extends PureComponent { return !result.error; }; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - if (event.id === START_BUTTON) { - this.startConversation(); - } else if (event.id === CLOSE_BUTTON) { - this.close(); - } - } - }; - onSearch = (text) => { if (text) { this.setState({term: text}); @@ -316,13 +326,14 @@ export default class MoreDirectMessages extends PureComponent { }; updateNavigationButtons = (startEnabled, context = this.context) => { + const {actions, componentId} = this.props; const {formatMessage} = context.intl; - this.props.navigator.setButtons({ + actions.setButtons(componentId, { rightButtons: [{ id: START_BUTTON, - title: formatMessage({id: 'mobile.more_dms.start', defaultMessage: 'Start'}), + text: formatMessage({id: 'mobile.more_dms.start', defaultMessage: 'Start'}), showAsAction: 'always', - disabled: !startEnabled, + enabled: startEnabled, }], }); }; diff --git a/app/screens/notification/index.js b/app/screens/notification/index.js index 7674cf74a..47d22b173 100644 --- a/app/screens/notification/index.js +++ b/app/screens/notification/index.js @@ -12,6 +12,8 @@ import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selector import {getUser} from 'mattermost-redux/selectors/entities/users'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {dismissOverlay, dismissAllModals, popToRoot} from 'app/actions/navigation'; + import Notification from './notification'; function mapStateToProps(state, ownProps) { @@ -42,6 +44,9 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ loadFromPushNotification, + dismissOverlay, + dismissAllModals, + popToRoot, }, dispatch), }; } diff --git a/app/screens/notification/notification.js b/app/screens/notification/notification.js index ff2225f00..732796e12 100644 --- a/app/screens/notification/notification.js +++ b/app/screens/notification/notification.js @@ -8,55 +8,149 @@ import { InteractionManager, Platform, StyleSheet, - Text, TouchableOpacity, + Text, View, } from 'react-native'; - -import FormattedText from 'app/components/formatted_text'; -import ProfilePicture from 'app/components/profile_picture'; -import {changeOpacity} from 'app/utils/theme'; +import {Navigation} from 'react-native-navigation'; +import * as Animatable from 'react-native-animatable'; +import {PanGestureHandler} from 'react-native-gesture-handler'; import {isDirectChannel} from 'mattermost-redux/utils/channel_utils'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import FormattedText from 'app/components/formatted_text'; +import ProfilePicture from 'app/components/profile_picture'; +import {changeOpacity} from 'app/utils/theme'; +import {NavigationTypes} from 'app/constants'; + import logo from 'assets/images/icon.png'; import webhookIcon from 'assets/images/icons/webhook.jpg'; const IMAGE_SIZE = 33; +const AUTO_DISMISS_TIME_MILLIS = 5000; export default class Notification extends PureComponent { static propTypes = { actions: PropTypes.shape({ loadFromPushNotification: PropTypes.func.isRequired, + dismissOverlay: PropTypes.func.isRequired, + dismissAllModals: PropTypes.func.isRequired, + popToRoot: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string.isRequired, channel: PropTypes.object, config: PropTypes.object, deviceWidth: PropTypes.number.isRequired, notification: PropTypes.object.isRequired, teammateNameDisplay: PropTypes.string, - navigator: PropTypes.object, theme: PropTypes.object.isRequired, user: PropTypes.object, }; + state = { + keyFrames: { + from: { + translateY: -100, + }, + to: { + translateY: 0, + }, + }, + } + + tapped = false; + + componentDidMount() { + this.setDismissTimer(); + this.setDidDisappearListener(); + this.setShowOverlayListener(); + } + + componentWillUnmount() { + this.clearDismissTimer(); + this.clearDidDisappearListener(); + this.clearShowOverlayListener(); + } + + setDismissTimer = () => { + this.dismissTimer = setTimeout(() => { + if (!this.tapped) { + this.animateDismissOverlay(); + } + }, AUTO_DISMISS_TIME_MILLIS); + } + + clearDismissTimer = () => { + if (this.dismissTimer) { + clearTimeout(this.dismissTimer); + this.dismissTimer = null; + } + } + + setDidDisappearListener = () => { + this.didDismissListener = Navigation.events().registerComponentDidDisappearListener(({componentId}) => { + if (componentId === this.props.componentId && this.tapped) { + const {actions} = this.props; + actions.dismissAllModals(); + actions.popToRoot(); + } + }); + } + + clearDidDisappearListener = () => { + this.didDismissListener.remove(); + } + + setShowOverlayListener = () => { + EventEmitter.on(NavigationTypes.NAVIGATION_SHOW_OVERLAY, this.onNewOverlay); + } + + clearShowOverlayListener = () => { + EventEmitter.off(NavigationTypes.NAVIGATION_SHOW_OVERLAY, this.onNewOverlay); + } + + onNewOverlay = () => { + // Dismiss this overlay so that there is only ever one. + this.dismissOverlay(); + } + + dismissOverlay = () => { + this.clearDismissTimer(); + + const {actions, componentId} = this.props; + actions.dismissOverlay(componentId); + } + + animateDismissOverlay = () => { + this.clearDismissTimer(); + + this.setState({ + keyFrames: { + from: { + translateY: 0, + }, + to: { + translateY: -100, + }, + }, + }); + setTimeout(() => this.dismissOverlay(), 1000); + } + notificationTapped = () => { - const {actions, navigator, notification} = this.props; + this.tapped = true; + this.clearDismissTimer(); + + const {actions, notification} = this.props; EventEmitter.emit('close_channel_drawer'); + EventEmitter.emit('close_settings_sidebar'); InteractionManager.runAfterInteractions(() => { - navigator.dismissInAppNotification(); + this.dismissOverlay(); if (!notification.localNotification) { actions.loadFromPushNotification(notification); - - if (Platform.OS === 'android') { - navigator.dismissModal({animation: 'none'}); - } - - navigator.popToRoot({ - animated: false, - }); } }); }; @@ -202,28 +296,39 @@ export default class Notification extends PureComponent { const icon = this.getNotificationIcon(); return ( - - + - - {icon} + + + + {icon} + + + {title} + + + {messageText} + + + + - - {title} - - - {messageText} - - - - - + + ); } diff --git a/app/screens/options_modal/index.js b/app/screens/options_modal/index.js index 0c73b392b..75216f0f6 100644 --- a/app/screens/options_modal/index.js +++ b/app/screens/options_modal/index.js @@ -1,8 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; +import {dismissModal} from 'app/actions/navigation'; + import {getDimensions} from 'app/selectors/device'; import OptionsModal from './options_modal'; @@ -13,4 +16,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(OptionsModal); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + dismissModal, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(OptionsModal); diff --git a/app/screens/options_modal/options_modal.js b/app/screens/options_modal/options_modal.js index 55835939d..d39a62f29 100644 --- a/app/screens/options_modal/options_modal.js +++ b/app/screens/options_modal/options_modal.js @@ -22,12 +22,13 @@ const DURATION = 200; export default class OptionsModal extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + dismissModal: PropTypes.func.isRequired, + }).isRequired, items: PropTypes.array.isRequired, deviceHeight: PropTypes.number.isRequired, deviceWidth: PropTypes.number.isRequired, - navigator: PropTypes.object, onCancelPress: PropTypes.func, - onItemPress: PropTypes.func, title: PropTypes.oneOfType([ PropTypes.string, PropTypes.object, @@ -36,7 +37,6 @@ export default class OptionsModal extends PureComponent { static defaultProps = { onCancelPress: emptyFunction, - onItemPress: emptyFunction, }; constructor(props) { @@ -69,16 +69,17 @@ export default class OptionsModal extends PureComponent { toValue: this.props.deviceHeight, duration: DURATION, }).start(() => { - this.props.navigator.dismissModal({ - animationType: 'none', - }); + this.props.actions.dismissModal(); }); }; + onItemPress = () => { + this.props.actions.dismissModal(); + } + render() { const { items, - onItemPress, title, } = this.props; @@ -89,7 +90,7 @@ export default class OptionsModal extends PureComponent { diff --git a/app/screens/permalink/index.js b/app/screens/permalink/index.js index 50d54399f..ef5f06525 100644 --- a/app/screens/permalink/index.js +++ b/app/screens/permalink/index.js @@ -16,13 +16,18 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import { + goToScreen, + dismissModal, + dismissAllModals, + resetToChannel, +} from 'app/actions/navigation'; import { handleSelectChannel, loadThreadIfNecessary, setChannelDisplayName, setChannelLoading, } from 'app/actions/views/channel'; -import {showSearchModal} from 'app/actions/views/search'; import {handleTeamChange} from 'app/actions/views/select_team'; import Permalink from './permalink'; @@ -74,7 +79,10 @@ function mapDispatchToProps(dispatch) { selectPost, setChannelDisplayName, setChannelLoading, - showSearchModal, + goToScreen, + dismissModal, + dismissAllModals, + resetToChannel, }, dispatch), }; } diff --git a/app/screens/permalink/permalink.js b/app/screens/permalink/permalink.js index 81f3f2763..3d59012b0 100644 --- a/app/screens/permalink/permalink.js +++ b/app/screens/permalink/permalink.js @@ -13,6 +13,7 @@ import {intlShape} from 'react-intl'; import * as Animatable from 'react-native-animatable'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; +import {Navigation} from 'react-native-navigation'; import {General} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; @@ -56,6 +57,10 @@ export default class Permalink extends PureComponent { selectPost: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, setChannelLoading: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + dismissAllModals: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, }).isRequired, channelId: PropTypes.string, channelIsArchived: PropTypes.bool, @@ -66,7 +71,6 @@ export default class Permalink extends PureComponent { focusedPostId: PropTypes.string.isRequired, isPermalink: PropTypes.bool, myMembers: PropTypes.object.isRequired, - navigator: PropTypes.object, onClose: PropTypes.func, onPress: PropTypes.func, postIds: PropTypes.array, @@ -131,8 +135,6 @@ export default class Permalink extends PureComponent { loading = false; } - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - this.state = { title: channelName, loading, @@ -146,6 +148,8 @@ export default class Permalink extends PureComponent { } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + this.mounted = true; if (this.state.loading) { @@ -163,40 +167,36 @@ export default class Permalink extends PureComponent { this.mounted = false; } + navigationButtonPressed({buttonId}) { + if (buttonId === 'backPress') { + this.handleClose(); + } + } + goToThread = preventDoubleTap((post) => { - const {actions, navigator, theme} = this.props; + const {actions} = this.props; const channelId = post.channel_id; const rootId = (post.root_id || post.id); + const screen = 'Thread'; + const title = ''; + const passProps = { + channelId, + rootId, + }; actions.loadThreadIfNecessary(rootId); actions.selectPost(rootId); - const options = { - screen: 'Thread', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelId, - rootId, - }, - }; - - navigator.push(options); + actions.goToScreen(screen, title, passProps); }); handleClose = () => { - const {actions, navigator, onClose} = this.props; + const {actions, onClose} = this.props; if (this.refs.view) { this.mounted = false; this.refs.view.zoomOut().then(() => { actions.selectPost(''); - navigator.dismissModal({animationType: 'none'}); + actions.dismissModal(); if (onClose) { onClose(); @@ -225,7 +225,7 @@ export default class Permalink extends PureComponent { jumpToChannel = (channelId, channelDisplayName) => { if (channelId) { - const {actions, channelTeamId, currentTeamId, navigator, onClose, theme} = this.props; + const {actions, channelTeamId, currentTeamId, onClose} = this.props; const currentChannelId = this.props.channelId; const { handleSelectChannel, @@ -239,23 +239,13 @@ export default class Permalink extends PureComponent { if (channelId === currentChannelId) { EventEmitter.emit('reset_channel'); } else { - navigator.resetTo({ - screen: 'Channel', - animated: true, - animationType: 'fade', - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - disableTermsModal: true, - }, - }); + const passProps = { + disableTermsModal: true, + }; + actions.resetToChannel(passProps); } - navigator.dismissAllModals({animationType: 'slide-down'}); + actions.dismissAllModals(); if (onClose) { onClose(); @@ -315,16 +305,6 @@ export default class Permalink extends PureComponent { } }; - onNavigatorEvent = (event) => { - switch (event.id) { - case 'backPress': - this.handleClose(); - break; - default: - break; - } - }; - retry = () => { if (this.mounted) { this.setState({loading: true, error: null, retry: false}); @@ -353,7 +333,6 @@ export default class Permalink extends PureComponent { const { currentUserId, focusedPostId, - navigator, theme, } = this.props; const { @@ -398,7 +377,6 @@ export default class Permalink extends PureComponent { lastPostIndex={Platform.OS === 'android' ? getLastPostIndex(postIdsState) : -1} currentUserId={currentUserId} lastViewedAt={0} - navigator={navigator} highlightPinnedOrFlagged={false} /> ); diff --git a/app/screens/permalink/permalink.test.js b/app/screens/permalink/permalink.test.js index e639645c6..bd1147c5a 100644 --- a/app/screens/permalink/permalink.test.js +++ b/app/screens/permalink/permalink.test.js @@ -11,14 +11,6 @@ import Permalink from './permalink.js'; jest.mock('react-intl'); describe('Permalink', () => { - const navigator = { - dismissAllModals: jest.fn(), - dismissModal: jest.fn(), - push: jest.fn(), - resetTo: jest.fn(), - setOnNavigatorEvent: jest.fn(), - }; - const actions = { getPostsAround: jest.fn(), getPostThread: jest.fn(), @@ -30,6 +22,10 @@ describe('Permalink', () => { selectPost: jest.fn(), setChannelDisplayName: jest.fn(), setChannelLoading: jest.fn(), + goToScreen: jest.fn(), + dismissModal: jest.fn(), + dismissAllModals: jest.fn(), + resetToChannel: jest.fn(), }; const baseProps = { @@ -43,11 +39,11 @@ describe('Permalink', () => { focusedPostId: 'focused_post_id', isPermalink: true, myMembers: {}, - navigator, onClose: jest.fn(), onPress: jest.fn(), postIds: ['post_id_1', 'focused_post_id', 'post_id_3'], theme: Preferences.THEMES.default, + componentId: 'component-id', }; test('should match snapshot', () => { @@ -82,7 +78,7 @@ describe('Permalink', () => { ); wrapper.instance().handleClose = jest.fn(); - wrapper.instance().onNavigatorEvent({id: 'backPress'}); + wrapper.instance().navigationButtonPressed({buttonId: 'backPress'}); expect(wrapper.instance().handleClose).toHaveBeenCalledTimes(1); }); diff --git a/app/screens/pinned_posts/index.js b/app/screens/pinned_posts/index.js index bfc5df20d..30f18d440 100644 --- a/app/screens/pinned_posts/index.js +++ b/app/screens/pinned_posts/index.js @@ -9,8 +9,13 @@ import {clearSearch, getPinnedPosts} from 'mattermost-redux/actions/search'; import {RequestStatus} from 'mattermost-redux/constants'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import { + dismissModal, + goToScreen, + showSearchModal, + showModalOverCurrentContext, +} from 'app/actions/navigation'; import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel'; -import {showSearchModal} from 'app/actions/views/search'; import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list'; import PinnedPosts from './pinned_posts'; @@ -43,7 +48,10 @@ function mapDispatchToProps(dispatch) { getPinnedPosts, selectFocusedPostId, selectPost, + dismissModal, + goToScreen, showSearchModal, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/screens/pinned_posts/pinned_posts.js b/app/screens/pinned_posts/pinned_posts.js index 121235508..b76c18b93 100644 --- a/app/screens/pinned_posts/pinned_posts.js +++ b/app/screens/pinned_posts/pinned_posts.js @@ -11,6 +11,7 @@ import { SafeAreaView, View, } from 'react-native'; +import {Navigation} from 'react-native-navigation'; import {isDateLine, getDateForDateLine} from 'mattermost-redux/utils/post_list'; @@ -34,12 +35,14 @@ export default class PinnedPosts extends PureComponent { getPinnedPosts: PropTypes.func.isRequired, selectFocusedPostId: PropTypes.func.isRequired, selectPost: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, showSearchModal: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }).isRequired, currentChannelId: PropTypes.string.isRequired, didFail: PropTypes.bool, isLoading: PropTypes.bool, - navigator: PropTypes.object, postIds: PropTypes.array, theme: PropTypes.object.isRequired, }; @@ -52,44 +55,34 @@ export default class PinnedPosts extends PureComponent { intl: intlShape.isRequired, }; - constructor(props) { - super(props); - - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - } - componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + const {actions, currentChannelId} = this.props; actions.clearSearch(); actions.getPinnedPosts(currentChannelId); } + navigationButtonPressed({buttonId}) { + if (buttonId === 'close-settings') { + this.props.actions.dismissModal(); + } + } + goToThread = (post) => { - const {actions, navigator, theme} = this.props; + const {actions} = this.props; const channelId = post.channel_id; const rootId = (post.root_id || post.id); - + const screen = 'Thread'; + const title = ''; + const passProps = { + channelId, + rootId, + }; Keyboard.dismiss(); actions.loadThreadIfNecessary(rootId); actions.selectPost(rootId); - - const options = { - screen: 'Thread', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelId, - rootId, - }, - }; - - navigator.push(options); + actions.goToScreen(screen, title, passProps); }; handleClosePermalink = () => { @@ -104,25 +97,15 @@ export default class PinnedPosts extends PureComponent { }; handleHashtagPress = async (hashtag) => { - const {actions, navigator} = this.props; + const {actions} = this.props; - await navigator.dismissModal(); + await actions.dismissModal(); - actions.showSearchModal(navigator, '#' + hashtag); + actions.showSearchModal('#' + hashtag); }; keyExtractor = (item) => item; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - if (event.id === 'close-settings') { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); - } - } - }; - previewPost = (post) => { Keyboard.dismiss(); @@ -170,7 +153,6 @@ export default class PinnedPosts extends PureComponent { previewPost={this.previewPost} highlightPinnedOrFlagged={true} goToThread={this.goToThread} - navigator={this.props.navigator} onHashtagPress={this.handleHashtagPress} onPermalinkPress={this.handlePermalinkPress} managedConfig={mattermostManaged.getCachedConfig()} @@ -184,29 +166,24 @@ export default class PinnedPosts extends PureComponent { }; showPermalinkView = (postId, isPermalink) => { - const {actions, navigator} = this.props; + const {actions} = this.props; actions.selectFocusedPostId(postId); if (!this.showingPermalink) { + const screen = 'Permalink'; + const passProps = { + isPermalink, + onClose: this.handleClosePermalink, + }; const options = { - screen: 'Permalink', - animationType: 'none', - backButtonTitle: '', - overrideBackPress: true, - navigatorStyle: { - navBarHidden: true, - screenBackgroundColor: changeOpacity('#000', 0.2), - modalPresentationStyle: 'overCurrentContext', - }, - passProps: { - isPermalink, - onClose: this.handleClosePermalink, + layout: { + backgroundColor: changeOpacity('#000', 0.2), }, }; this.showingPermalink = true; - navigator.showModal(options); + actions.showModalOverCurrentContext(screen, passProps, options); } }; diff --git a/app/screens/post_options/index.js b/app/screens/post_options/index.js index aa0fd3b7f..e6120ef8e 100644 --- a/app/screens/post_options/index.js +++ b/app/screens/post_options/index.js @@ -22,6 +22,7 @@ import {getCurrentTeamId, getCurrentTeamUrl} from 'mattermost-redux/selectors/en import {canEditPost} from 'mattermost-redux/utils/post_utils'; import {THREAD} from 'app/constants/screen'; +import {dismissModal, showModal} from 'app/actions/navigation'; import {addReaction} from 'app/actions/views/emoji'; import {getDimensions} from 'app/selectors/device'; @@ -121,6 +122,8 @@ function mapDispatchToProps(dispatch) { removePost, unflagPost, unpinPost, + dismissModal, + showModal, }, dispatch), }; } diff --git a/app/screens/post_options/post_options.js b/app/screens/post_options/post_options.js index 8f2be0ad7..64c934ebd 100644 --- a/app/screens/post_options/post_options.js +++ b/app/screens/post_options/post_options.js @@ -25,6 +25,8 @@ export default class PostOptions extends PureComponent { removePost: PropTypes.func.isRequired, unflagPost: PropTypes.func.isRequired, unpinPost: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + showModal: PropTypes.func.isRequired, }).isRequired, canAddReaction: PropTypes.bool, canReply: PropTypes.bool, @@ -39,7 +41,6 @@ export default class PostOptions extends PureComponent { deviceHeight: PropTypes.number.isRequired, isFlagged: PropTypes.bool, isMyPost: PropTypes.bool, - navigator: PropTypes.object.isRequired, post: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, }; @@ -49,9 +50,7 @@ export default class PostOptions extends PureComponent { }; close = () => { - this.props.navigator.dismissModal({ - animationType: 'none', - }); + this.props.actions.dismissModal(); }; closeWithAnimation = () => { @@ -266,27 +265,20 @@ export default class PostOptions extends PureComponent { }; handleAddReaction = () => { + const {actions, theme} = this.props; const {formatMessage} = this.context.intl; - const {navigator, theme} = this.props; this.close(); setTimeout(() => { MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => { - navigator.showModal({ - screen: 'AddReaction', - title: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - closeButton: source, - onEmojiPress: this.handleAddReactionToPost, - }, - }); + const screen = 'AddReaction'; + const title = formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}); + const passProps = { + closeButton: source, + onEmojiPress: this.handleAddReactionToPost, + }; + + actions.showModal(screen, title, passProps); }); }, 300); }; @@ -364,27 +356,20 @@ export default class PostOptions extends PureComponent { }; handlePostEdit = () => { + const {actions, theme, post} = this.props; const {intl} = this.context; - const {navigator, post, theme} = this.props; this.close(); setTimeout(() => { 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, - }, - }); + const screen = 'EditPost'; + const title = intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'}); + const passProps = { + post, + closeButton: source, + }; + + actions.showModal(screen, title, passProps); }); }, 300); }; diff --git a/app/screens/post_options/post_options.test.js b/app/screens/post_options/post_options.test.js index dc0498c73..cd742c97f 100644 --- a/app/screens/post_options/post_options.test.js +++ b/app/screens/post_options/post_options.test.js @@ -18,12 +18,6 @@ jest.mock('Alert', () => { }); describe('PostOptions', () => { - const navigator = { - showModal: jest.fn(), - dismissModal: jest.fn(), - push: jest.fn(), - }; - const actions = { addReaction: jest.fn(), deletePost: jest.fn(), @@ -32,6 +26,8 @@ describe('PostOptions', () => { removePost: jest.fn(), unflagPost: jest.fn(), unpinPost: jest.fn(), + dismissModal: jest.fn(), + showModal: jest.fn(), }; const post = { @@ -58,7 +54,6 @@ describe('PostOptions', () => { isMyPost: true, isSystemMessage: false, managedConfig: {}, - navigator, post, showAddReaction: true, theme: Preferences.THEMES.default, diff --git a/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap b/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap index 60ce984e2..bdfdeb767 100644 --- a/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap +++ b/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap @@ -22,25 +22,8 @@ exports[`ReactionList should match snapshot 1`] = ` } } > - - - - { - if (event.type === 'NavBarButtonPress') { - if (event.id === 'close-reaction-list') { - this.close(); - } + navigationButtonPressed({buttonId}) { + if (buttonId === 'close-reaction-list') { + this.close(); } - }; + } close = () => { - this.props.navigator.dismissModal({ - animationType: 'none', - }); + this.props.actions.dismissModal(); }; getMissingProfiles = () => { @@ -138,7 +135,6 @@ export default class ReactionList extends PureComponent { renderReactionRows = () => { const { - navigator, teammateNameDisplay, theme, } = this.props; @@ -158,7 +154,6 @@ export default class ReactionList extends PureComponent { > { const baseProps = { actions: { getMissingProfilesByIds: jest.fn(), + dismissModal: jest.fn(), }, allUserIds: ['user_id_1', 'user_id_2'], - navigator: {setOnNavigatorEvent: jest.fn()}, reactions: {'user_id_1-smile': {emoji_name: 'smile', user_id: 'user_id_1'}, 'user_id_2-+1': {emoji_name: '+1', user_id: 'user_id_2'}}, theme: Preferences.THEMES.default, teammateNameDisplay: 'username', userProfiles: [{id: 'user_id_1', username: 'username_1'}, {id: 'user_id_2', username: 'username_2'}], + componentId: 'component-id', }; test('should match snapshot', () => { diff --git a/app/screens/reaction_list/__snapshots__/reaction_row.test.js.snap b/app/screens/reaction_list/reaction_row/__snapshots__/reaction_row.test.js.snap similarity index 100% rename from app/screens/reaction_list/__snapshots__/reaction_row.test.js.snap rename to app/screens/reaction_list/reaction_row/__snapshots__/reaction_row.test.js.snap diff --git a/app/screens/reaction_list/reaction_row/index.js b/app/screens/reaction_list/reaction_row/index.js new file mode 100644 index 000000000..d91992b1f --- /dev/null +++ b/app/screens/reaction_list/reaction_row/index.js @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {goToScreen} from 'app/actions/navigation'; + +import ReactionRow from './reaction_row'; + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(null, mapDispatchToProps)(ReactionRow); diff --git a/app/screens/reaction_list/reaction_row.js b/app/screens/reaction_list/reaction_row/reaction_row.js similarity index 82% rename from app/screens/reaction_list/reaction_row.js rename to app/screens/reaction_list/reaction_row/reaction_row.js index da33b73ff..8ad6e2e56 100644 --- a/app/screens/reaction_list/reaction_row.js +++ b/app/screens/reaction_list/reaction_row/reaction_row.js @@ -21,8 +21,10 @@ import Emoji from 'app/components/emoji'; export default class ReactionRow extends React.PureComponent { static propTypes = { + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, emojiName: PropTypes.string.isRequired, - navigator: PropTypes.object, teammateNameDisplay: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, user: PropTypes.object.isRequired, @@ -37,26 +39,15 @@ export default class ReactionRow extends React.PureComponent { }; goToUserProfile = () => { - const {navigator, theme, user} = this.props; + const {actions, user} = this.props; const {formatMessage} = this.context.intl; - - const options = { - screen: 'UserProfile', - title: formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}), - animated: true, - backButtonTitle: '', - passProps: { - userId: user.id, - }, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, + const screen = 'UserProfile'; + const title = formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); + const passProps = { + userId: user.id, }; - navigator.push(options); + actions.goToScreen(screen, title, passProps); }; render() { diff --git a/app/screens/reaction_list/reaction_row.test.js b/app/screens/reaction_list/reaction_row/reaction_row.test.js similarity index 91% rename from app/screens/reaction_list/reaction_row.test.js rename to app/screens/reaction_list/reaction_row/reaction_row.test.js index 471b67658..21e7fcd10 100644 --- a/app/screens/reaction_list/reaction_row.test.js +++ b/app/screens/reaction_list/reaction_row/reaction_row.test.js @@ -9,8 +9,10 @@ import ReactionRow from './reaction_row'; describe('ReactionRow', () => { const baseProps = { + actions: { + goToScreen: jest.fn(), + }, emojiName: 'smile', - navigator: {}, teammateNameDisplay: 'username', theme: Preferences.THEMES.default, user: {id: 'user_id', username: 'username'}, diff --git a/app/screens/recent_mentions/index.js b/app/screens/recent_mentions/index.js index 914e756f2..7ee56b2ef 100644 --- a/app/screens/recent_mentions/index.js +++ b/app/screens/recent_mentions/index.js @@ -9,8 +9,13 @@ import {clearSearch, getRecentMentions} from 'mattermost-redux/actions/search'; import {RequestStatus} from 'mattermost-redux/constants'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import { + dismissModal, + goToScreen, + showSearchModal, + showModalOverCurrentContext, +} from 'app/actions/navigation'; import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel'; -import {showSearchModal} from 'app/actions/views/search'; import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list'; import RecentMentions from './recent_mentions'; @@ -42,6 +47,9 @@ function mapDispatchToProps(dispatch) { selectFocusedPostId, selectPost, showSearchModal, + dismissModal, + goToScreen, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/screens/recent_mentions/recent_mentions.js b/app/screens/recent_mentions/recent_mentions.js index 2bcef54d1..9fd2ba7a1 100644 --- a/app/screens/recent_mentions/recent_mentions.js +++ b/app/screens/recent_mentions/recent_mentions.js @@ -11,6 +11,7 @@ import { StyleSheet, View, } from 'react-native'; +import {Navigation} from 'react-native-navigation'; import {isDateLine, getDateForDateLine} from 'mattermost-redux/utils/post_list'; @@ -35,10 +36,11 @@ export default class RecentMentions extends PureComponent { selectFocusedPostId: PropTypes.func.isRequired, selectPost: PropTypes.func.isRequired, showSearchModal: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }).isRequired, didFail: PropTypes.bool, isLoading: PropTypes.bool, - navigator: PropTypes.object, postIds: PropTypes.array, theme: PropTypes.object.isRequired, }; @@ -54,37 +56,29 @@ export default class RecentMentions extends PureComponent { constructor(props) { super(props); - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); props.actions.clearSearch(); props.actions.getRecentMentions(); } + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + } + goToThread = (post) => { - const {actions, navigator, theme} = this.props; + const {actions} = this.props; const channelId = post.channel_id; const rootId = (post.root_id || post.id); + const screen = 'Thread'; + const title = ''; + const passProps = { + channelId, + rootId, + }; Keyboard.dismiss(); actions.loadThreadIfNecessary(rootId); actions.selectPost(rootId); - - const options = { - screen: 'Thread', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelId, - rootId, - }, - }; - - navigator.push(options); + actions.goToScreen(screen, title, passProps); }; handleClosePermalink = () => { @@ -99,24 +93,20 @@ export default class RecentMentions extends PureComponent { }; handleHashtagPress = async (hashtag) => { - const {actions, navigator} = this.props; + const {actions} = this.props; - await navigator.dismissModal(); + await actions.dismissModal(); - actions.showSearchModal(navigator, '#' + hashtag); + actions.showSearchModal('#' + hashtag); }; keyExtractor = (item) => item; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - if (event.id === 'close-settings') { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); - } + navigationButtonPressed({buttonId}) { + if (buttonId === 'close-settings') { + this.props.actions.dismissModal(); } - }; + } previewPost = (post) => { Keyboard.dismiss(); @@ -166,7 +156,6 @@ export default class RecentMentions extends PureComponent { postId={item} previewPost={this.previewPost} goToThread={this.goToThread} - navigator={this.props.navigator} onHashtagPress={this.handleHashtagPress} onPermalinkPress={this.handlePermalinkPress} managedConfig={mattermostManaged.getCachedConfig()} @@ -179,29 +168,24 @@ export default class RecentMentions extends PureComponent { }; showPermalinkView = (postId, isPermalink) => { - const {actions, navigator} = this.props; + const {actions} = this.props; actions.selectFocusedPostId(postId); if (!this.showingPermalink) { + const screen = 'Permalink'; + const passProps = { + isPermalink, + onClose: this.handleClosePermalink, + }; const options = { - screen: 'Permalink', - animationType: 'none', - backButtonTitle: '', - overrideBackPress: true, - navigatorStyle: { - navBarHidden: true, - screenBackgroundColor: changeOpacity('#000', 0.2), - modalPresentationStyle: 'overCurrentContext', - }, - passProps: { - isPermalink, - onClose: this.handleClosePermalink, + layout: { + backgroundColor: changeOpacity('#000', 0.2), }, }; this.showingPermalink = true; - navigator.showModal(options); + actions.showModalOverCurrentContext(screen, passProps, options); } }; diff --git a/app/screens/search/index.js b/app/screens/search/index.js index c7194a20c..7f6918eae 100644 --- a/app/screens/search/index.js +++ b/app/screens/search/index.js @@ -8,18 +8,19 @@ import {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts'; import {clearSearch, removeSearchTerms, searchPostsWithParams, getMorePostsForSearch} from 'mattermost-redux/actions/search'; import {getCurrentChannelId, filterPostIds} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {dismissModal, goToScreen, showModalOverCurrentContext} from 'app/actions/navigation'; import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel'; +import {handleSearchDraftChanged} from 'app/actions/views/search'; import {isLandscape} from 'app/selectors/device'; import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list'; -import {handleSearchDraftChanged} from 'app/actions/views/search'; import {getDeviceUtcOffset, getUtcOffsetForTimeZone} from 'app/utils/timezone'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; import Search from './search'; @@ -84,6 +85,9 @@ function mapDispatchToProps(dispatch) { searchPostsWithParams, getMorePostsForSearch, selectPost, + dismissModal, + goToScreen, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/screens/search/search.js b/app/screens/search/search.js index 9e98ddd73..d8d32a2f7 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -12,6 +12,7 @@ import { View, } from 'react-native'; import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; +import {Navigation} from 'react-native-navigation'; import {debounce} from 'mattermost-redux/actions/helpers'; import {RequestStatus} from 'mattermost-redux/constants'; @@ -55,11 +56,14 @@ export default class Search extends PureComponent { getMorePostsForSearch: PropTypes.func.isRequired, selectFocusedPostId: PropTypes.func.isRequired, selectPost: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string.isRequired, currentTeamId: PropTypes.string.isRequired, initialValue: PropTypes.string, isLandscape: PropTypes.bool.isRequired, - navigator: PropTypes.object, postIds: PropTypes.array, archivedPostIds: PropTypes.arrayOf(PropTypes.string), recent: PropTypes.array.isRequired, @@ -85,8 +89,6 @@ export default class Search extends PureComponent { constructor(props) { super(props); - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - this.contentOffsetY = 0; this.state = { channelName: '', @@ -97,6 +99,8 @@ export default class Search extends PureComponent { } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + if (this.props.initialValue) { this.search(this.props.initialValue); } @@ -140,6 +144,16 @@ export default class Search extends PureComponent { } } + navigationButtonPressed({buttonId}) { + if (buttonId === 'backPress') { + if (this.state.preview) { + this.refs.preview.handleClose(); + } else { + this.props.actions.dismissModal(); + } + } + } + archivedIndicator = (postID, style) => { const channelIsArchived = this.props.archivedPostIds.includes(postID); let archivedIndicator = null; @@ -165,13 +179,12 @@ export default class Search extends PureComponent { }; cancelSearch = preventDoubleTap(() => { - const {navigator} = this.props; this.handleTextChanged('', true); - navigator.dismissModal({animationType: 'slide-down'}); + this.props.actions.dismissModal(); }); goToThread = (post) => { - const {actions, navigator, theme} = this.props; + const {actions} = this.props; const channelId = post.channel_id; const rootId = (post.root_id || post.id); @@ -179,28 +192,19 @@ export default class Search extends PureComponent { actions.loadThreadIfNecessary(rootId); actions.selectPost(rootId); - const options = { - screen: 'Thread', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelId, - rootId, - }, + const screen = 'Thread'; + const title = ''; + const passProps = { + channelId, + rootId, }; - navigator.push(options); + actions.goToScreen(screen, title, passProps); }; handleHashtagPress = (hashtag) => { if (this.showingPermalink) { - this.props.navigator.dismissModal(); + this.props.actions.dismissModal(); this.handleClosePermalink(); } @@ -298,16 +302,6 @@ export default class Search extends PureComponent { } }, 100); - onNavigatorEvent = (event) => { - if (event.id === 'backPress') { - if (this.state.preview) { - this.refs.preview.handleClose(); - } else { - this.props.navigator.dismissModal(); - } - } - }; - previewPost = (post) => { Keyboard.dismiss(); @@ -387,7 +381,6 @@ export default class Search extends PureComponent { postId={item} previewPost={this.previewPost} goToThread={this.goToThread} - navigator={this.props.navigator} onHashtagPress={this.handleHashtagPress} onPermalinkPress={this.handlePermalinkPress} managedConfig={mattermostManaged.getCachedConfig()} @@ -447,29 +440,24 @@ export default class Search extends PureComponent { }; showPermalinkView = (postId, isPermalink) => { - const {actions, navigator} = this.props; + const {actions} = this.props; actions.selectFocusedPostId(postId); if (!this.showingPermalink) { + const screen = 'Permalink'; + const passProps = { + isPermalink, + onClose: this.handleClosePermalink, + }; const options = { - screen: 'Permalink', - animationType: 'none', - backButtonTitle: '', - overrideBackPress: true, - navigatorStyle: { - navBarHidden: true, - screenBackgroundColor: changeOpacity('#000', 0.2), - modalPresentationStyle: 'overCurrentContext', - }, - passProps: { - isPermalink, - onClose: this.handleClosePermalink, + layout: { + backgroundColor: changeOpacity('#000', 0.2), }, }; this.showingPermalink = true; - navigator.showModal(options); + actions.showModalOverCurrentContext(screen, passProps, options); } }; diff --git a/app/screens/search/search_result_post/search_result_post.js b/app/screens/search/search_result_post/search_result_post.js index 6e4481b1d..dc8e60261 100644 --- a/app/screens/search/search_result_post/search_result_post.js +++ b/app/screens/search/search_result_post/search_result_post.js @@ -12,7 +12,6 @@ export default class SearchResultPost extends PureComponent { goToThread: PropTypes.func.isRequired, highlightPinnedOrFlagged: PropTypes.bool, managedConfig: PropTypes.object.isRequired, - navigator: PropTypes.object.isRequired, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func.isRequired, postId: PropTypes.string.isRequired, @@ -50,7 +49,6 @@ export default class SearchResultPost extends PureComponent { isSearchResult={true} showAddReaction={false} showFullDate={this.props.showFullDate} - navigator={this.props.navigator} /> ); } diff --git a/app/screens/select_server/index.js b/app/screens/select_server/index.js index 99ef53694..8bc27e0cf 100644 --- a/app/screens/select_server/index.js +++ b/app/screens/select_server/index.js @@ -6,14 +6,14 @@ import {connect} from 'react-redux'; import {getPing, resetPing, setServerVersion} from 'mattermost-redux/actions/general'; import {login} from 'mattermost-redux/actions/users'; +import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {resetToChannel, goToScreen} from 'app/actions/navigation'; import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; import {handleSuccessfulLogin, scheduleExpiredNotification} from 'app/actions/views/login'; import {loadConfigAndLicense} from 'app/actions/views/root'; import {handleServerUrlChanged} from 'app/actions/views/select_server'; import getClientUpgrade from 'app/selectors/client_upgrade'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; import SelectServer from './select_server'; @@ -30,7 +30,6 @@ function mapStateToProps(state) { latestVersion, license, minVersion, - theme: getTheme(state), }; } @@ -46,6 +45,8 @@ function mapDispatchToProps(dispatch) { resetPing, setLastUpgradeCheck, setServerVersion, + resetToChannel, + goToScreen, }, dispatch), }; } diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index f65c1de77..a51d25539 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; +import {Navigation} from 'react-native-navigation'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; import { @@ -21,6 +22,8 @@ import { import Button from 'react-native-button'; import RNFetchBlob from 'rn-fetch-blob'; +import merge from 'deepmerge'; + import {Client4} from 'mattermost-redux/client'; import ErrorText from 'app/components/error_text'; @@ -48,6 +51,7 @@ export default class SelectServer extends PureComponent { loadConfigAndLicense: PropTypes.func.isRequired, login: PropTypes.func.isRequired, resetPing: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, setLastUpgradeCheck: PropTypes.func.isRequired, setServerVersion: PropTypes.func.isRequired, }).isRequired, @@ -58,9 +62,7 @@ export default class SelectServer extends PureComponent { latestVersion: PropTypes.string, license: PropTypes.object, minVersion: PropTypes.string, - navigator: PropTypes.object, serverUrl: PropTypes.string.isRequired, - theme: PropTypes.object, }; static contextTypes = { @@ -81,6 +83,8 @@ export default class SelectServer extends PureComponent { } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + const {allowOtherServers, serverUrl} = this.props; if (!allowOtherServers && serverUrl) { // If the app is managed or AutoSelectServerUrl is true in the Config, the server url is set and the user can't change it @@ -93,7 +97,6 @@ export default class SelectServer extends PureComponent { } this.certificateListener = DeviceEventEmitter.addListener('RNFetchBlobCertificate', this.selectCertificate); - this.props.navigator.setOnNavigatorEvent(this.handleNavigatorEvent); telemetry.end(['start:select_server_screen']); telemetry.save(); @@ -117,10 +120,19 @@ export default class SelectServer extends PureComponent { } componentWillUnmount() { - this.certificateListener.remove(); if (Platform.OS === 'android') { Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); } + + this.certificateListener.remove(); + + this.navigationEventListener.remove(); + } + + componentDidDisappear() { + this.setState({ + connected: false, + }); } blur = () => { @@ -144,21 +156,18 @@ export default class SelectServer extends PureComponent { return stripTrailingSlashes(preUrl.protocol + '//' + preUrl.host + preUrl.pathname); }; - goToNextScreen = (screen, title) => { - const {navigator, theme} = this.props; - navigator.push({ - screen, - title, - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarHidden: LocalConfig.AutoSelectServerUrl, - disabledBackGesture: LocalConfig.AutoSelectServerUrl, - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, + goToNextScreen = (screen, title, passProps = {}, navOptions = {}) => { + const {actions} = this.props; + const defaultOptions = { + popGesture: !LocalConfig.AutoSelectServerUrl, + topBar: { + visible: !LocalConfig.AutoSelectServerUrl, + height: LocalConfig.AutoSelectServerUrl ? 0 : null, }, - }); + }; + const options = merge(defaultOptions, navOptions); + + actions.goToScreen(screen, title, passProps, options); }; handleAndroidKeyboard = () => { @@ -244,38 +253,21 @@ export default class SelectServer extends PureComponent { } }; - handleNavigatorEvent = (event) => { - switch (event.id) { - case 'didDisappear': - this.setState({ - connected: false, - }); - break; - } - }; - handleShowClientUpgrade = (upgradeType) => { const {formatMessage} = this.context.intl; - const {theme} = this.props; + const screen = 'ClientUpgrade'; + const title = formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Client Upgrade'}); + const passProps = { + closeAction: this.handleLoginOptions, + upgradeType, + }; + const options = { + statusBar: { + visible: false, + }, + }; - this.props.navigator.push({ - screen: 'ClientUpgrade', - title: formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Client Upgrade'}), - backButtonTitle: '', - navigatorStyle: { - navBarHidden: LocalConfig.AutoSelectServerUrl, - disabledBackGesture: LocalConfig.AutoSelectServerUrl, - statusBarHidden: true, - statusBarHideWithNavBar: true, - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - }, - passProps: { - closeAction: this.handleLoginOptions, - upgradeType, - }, - }); + this.goToNextScreen(screen, title, passProps, options); }; handleTextChanged = (url) => { @@ -287,28 +279,13 @@ export default class SelectServer extends PureComponent { }; loginWithCertificate = async () => { - const {navigator} = this.props; - tracker.initialLoad = Date.now(); await this.props.actions.login('credential', 'password'); await this.props.actions.handleSuccessfulLogin(); this.scheduleSessionExpiredNotification(); - navigator.resetTo({ - screen: 'Channel', - title: '', - animated: false, - backButtonTitle: '', - navigatorStyle: { - animated: true, - animationType: 'fade', - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - }, - }); + this.props.actions.resetToChannel(); }; pingServer = (url, retryWithHttp = true) => { diff --git a/app/screens/select_team/index.js b/app/screens/select_team/index.js index edc3c6f2a..cfbe925f1 100644 --- a/app/screens/select_team/index.js +++ b/app/screens/select_team/index.js @@ -4,12 +4,13 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {handleTeamChange} from 'app/actions/views/select_team'; - import {getTeams, joinTeam} from 'mattermost-redux/actions/teams'; import {logout} from 'mattermost-redux/actions/users'; import {getJoinableTeams} from 'mattermost-redux/selectors/entities/teams'; +import {resetToChannel, dismissModal} from 'app/actions/navigation'; +import {handleTeamChange} from 'app/actions/views/select_team'; + import SelectTeam from './select_team.js'; function mapStateToProps(state) { @@ -26,6 +27,8 @@ function mapDispatchToProps(dispatch) { handleTeamChange, joinTeam, logout, + resetToChannel, + dismissModal, }, dispatch), }; } diff --git a/app/screens/select_team/select_team.js b/app/screens/select_team/select_team.js index d6d9971b8..2e0866aca 100644 --- a/app/screens/select_team/select_team.js +++ b/app/screens/select_team/select_team.js @@ -10,6 +10,7 @@ import { TouchableOpacity, View, } from 'react-native'; +import {Navigation} from 'react-native-navigation'; import {RequestStatus} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; @@ -44,9 +45,11 @@ export default class SelectTeam extends PureComponent { handleTeamChange: PropTypes.func.isRequired, joinTeam: PropTypes.func.isRequired, logout: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string.isRequired, currentUrl: PropTypes.string.isRequired, - navigator: PropTypes.object, userWithoutTeams: PropTypes.bool, teams: PropTypes.array.isRequired, theme: PropTypes.object, @@ -59,7 +62,6 @@ export default class SelectTeam extends PureComponent { constructor(props) { super(props); - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); this.state = { loading: false, @@ -71,12 +73,14 @@ export default class SelectTeam extends PureComponent { } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + this.getTeams(); } componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } if (this.props.teams !== nextProps.teams) { @@ -84,6 +88,19 @@ export default class SelectTeam extends PureComponent { } } + navigationButtonPressed({buttonId}) { + const {logout} = this.props.actions; + + switch (buttonId) { + case 'close-teams': + this.close(); + break; + case 'logout': + InteractionManager.runAfterInteractions(logout); + break; + } + } + getTeams = () => { this.setState({loading: true}); this.props.actions.getTeams(this.state.page, TEAMS_PER_PAGE).then(() => { @@ -108,42 +125,15 @@ export default class SelectTeam extends PureComponent { }; close = () => { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); + this.props.actions.dismissModal(); }; goToChannelView = () => { - const {navigator, theme} = this.props; + const passProps = { + disableTermsModal: true, + }; - navigator.resetTo({ - screen: 'Channel', - animated: false, - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - disableTermsModal: true, - }, - }); - }; - - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - const {logout} = this.props.actions; - - switch (event.id) { - case 'close-teams': - this.close(); - break; - case 'logout': - InteractionManager.runAfterInteractions(logout); - break; - } - } + this.props.actions.resetToChannel(passProps); }; onSelectTeam = async (team) => { diff --git a/app/screens/select_team/select_team.test.js b/app/screens/select_team/select_team.test.js index c722e2f20..7e3a1219b 100644 --- a/app/screens/select_team/select_team.test.js +++ b/app/screens/select_team/select_team.test.js @@ -29,21 +29,21 @@ describe('SelectTeam', () => { handleTeamChange: jest.fn(), joinTeam: jest.fn(), logout: jest.fn(), + resetToChannel: jest.fn(), + dismissModal: jest.fn(), }; const baseProps = { actions, currentChannelId: 'someId', currentUrl: 'test', - navigator: { - setOnNavigatorEvent: jest.fn(), - }, userWithoutTeams: false, teams: [], theme: Preferences.THEMES.default, teamsRequest: { status: RequestStatus.FAILURE, }, + componentId: 'component-id', }; test('should match snapshot for fail of teams', async () => { diff --git a/app/screens/selector_screen/index.js b/app/screens/selector_screen/index.js index e5f69d1fd..1bd00b4ac 100644 --- a/app/screens/selector_screen/index.js +++ b/app/screens/selector_screen/index.js @@ -9,6 +9,8 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getProfiles, searchProfiles} from 'mattermost-redux/actions/users'; import {getChannels, searchChannels} from 'mattermost-redux/actions/channels'; +import {popTopScreen} from 'app/actions/navigation'; + import SelectorScreen from './selector_screen'; function mapStateToProps(state) { @@ -32,6 +34,7 @@ function mapDispatchToProps(dispatch) { getChannels, searchProfiles, searchChannels, + popTopScreen, }, dispatch), }; } diff --git a/app/screens/selector_screen/selector_screen.js b/app/screens/selector_screen/selector_screen.js index 39037bab2..9db461989 100644 --- a/app/screens/selector_screen/selector_screen.js +++ b/app/screens/selector_screen/selector_screen.js @@ -34,11 +34,12 @@ export default class SelectorScreen extends PureComponent { getChannels: PropTypes.func.isRequired, searchProfiles: PropTypes.func.isRequired, searchChannels: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, }), + componentId: PropTypes.string, currentTeamId: PropTypes.string.isRequired, data: PropTypes.arrayOf(PropTypes.object), dataSource: PropTypes.string, - navigator: PropTypes.object, onSelect: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, }; @@ -65,8 +66,6 @@ export default class SelectorScreen extends PureComponent { searchResults: [], term: '', }; - - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); } componentDidMount() { @@ -85,7 +84,7 @@ export default class SelectorScreen extends PureComponent { componentDidUpdate(prevProps) { if (this.props.theme !== prevProps.theme) { - setNavigatorStyles(this.props.navigator, this.props.theme); + setNavigatorStyles(this.props.componentId, this.props.theme); } } @@ -94,7 +93,7 @@ export default class SelectorScreen extends PureComponent { }; close = () => { - this.props.navigator.pop({animated: true}); + this.props.actions.popTopScreen(); }; handleSelectItem = (id, item) => { diff --git a/app/screens/selector_screen/selector_screen.test.js b/app/screens/selector_screen/selector_screen.test.js index a63b9239d..eb6d311cb 100644 --- a/app/screens/selector_screen/selector_screen.test.js +++ b/app/screens/selector_screen/selector_screen.test.js @@ -67,14 +67,12 @@ describe('SelectorScreen', () => { getChannels, searchProfiles, searchChannels, + popTopScreen: jest.fn(), }; const baseProps = { actions, currentTeamId: 'someId', - navigator: { - setOnNavigatorEvent: jest.fn(), - }, onSelect: jest.fn(), data: [{text: 'text', value: 'value'}], dataSource: null, diff --git a/app/screens/settings/display_settings/display_settings.js b/app/screens/settings/display_settings/display_settings.js index 02c920e8e..a4ca423c5 100644 --- a/app/screens/settings/display_settings/display_settings.js +++ b/app/screens/settings/display_settings/display_settings.js @@ -12,13 +12,16 @@ import { import SettingsItem from 'app/screens/settings/settings_item'; import StatusBar from 'app/components/status_bar'; import {preventDoubleTap} from 'app/utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import ClockDisplay from 'app/screens/clock_display'; export default class DisplaySettings extends PureComponent { static propTypes = { - navigator: PropTypes.object.isRequired, + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, + componentId: PropTypes.string, theme: PropTypes.object.isRequired, enableTheme: PropTypes.bool.isRequired, enableTimezone: PropTypes.bool.isRequired, @@ -32,32 +35,18 @@ export default class DisplaySettings extends PureComponent { showClockDisplaySettings: false, }; - constructor(props) { - super(props); - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - } - closeClockDisplaySettings = () => { this.setState({showClockDisplaySettings: false}); }; goToClockDisplaySettings = preventDoubleTap(() => { - const {navigator, theme} = this.props; + const {actions} = this.props; const {intl} = this.context; if (Platform.OS === 'ios') { - navigator.push({ - screen: 'ClockDisplay', - title: intl.formatMessage({id: 'user.settings.display.clockDisplay', defaultMessage: 'Clock Display'}), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }); + const screen = 'ClockDisplay'; + const title = intl.formatMessage({id: 'user.settings.display.clockDisplay', defaultMessage: 'Clock Display'}); + actions.goToScreen(screen, title); return; } @@ -65,47 +54,23 @@ export default class DisplaySettings extends PureComponent { }); goToTimezoneSettings = preventDoubleTap(() => { - const {navigator, theme} = this.props; + const {actions} = this.props; const {intl} = this.context; + const screen = 'TimezoneSettings'; + const title = intl.formatMessage({id: 'mobile.advanced_settings.timezone', defaultMessage: 'Timezone'}); - navigator.push({ - screen: 'TimezoneSettings', - title: intl.formatMessage({id: 'mobile.advanced_settings.timezone', defaultMessage: 'Timezone'}), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }); + actions.goToScreen(screen, title); }); goToThemeSettings = preventDoubleTap(() => { - const {navigator, theme} = this.props; + const {actions} = this.props; const {intl} = this.context; + const screen = 'ThemeSettings'; + const title = intl.formatMessage({id: 'mobile.display_settings.theme', defaultMessage: 'Theme'}); - navigator.push({ - screen: 'ThemeSettings', - title: intl.formatMessage({id: 'mobile.display_settings.theme', defaultMessage: 'Theme'}), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }); + actions.goToScreen(screen, title); }); - onNavigatorEvent = (event) => { - if (event.id === 'willAppear') { - setNavigatorStyles(this.props.navigator, this.props.theme); - } - }; - render() { const {theme, enableTimezone, enableTheme} = this.props; const {showClockDisplaySettings} = this.state; diff --git a/app/screens/settings/display_settings/display_settings.test.js b/app/screens/settings/display_settings/display_settings.test.js index 1d1b40804..54ae73fd7 100644 --- a/app/screens/settings/display_settings/display_settings.test.js +++ b/app/screens/settings/display_settings/display_settings.test.js @@ -12,13 +12,13 @@ jest.mock('react-intl'); describe('DisplaySettings', () => { const baseProps = { + actions: { + goToScreen: jest.fn(), + }, theme: Preferences.THEMES.default, enableTheme: false, enableTimezone: false, - navigator: { - push: jest.fn(), - setOnNavigatorEvent: jest.fn(), - }, + componentId: 'component-id', }; test('should match snapshot', () => { diff --git a/app/screens/settings/display_settings/index.js b/app/screens/settings/display_settings/index.js index 9c9a6c80c..12d4b321f 100644 --- a/app/screens/settings/display_settings/index.js +++ b/app/screens/settings/display_settings/index.js @@ -1,15 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; +import {goToScreen} from 'app/actions/navigation'; +import {getAllowedThemes} from 'app/selectors/theme'; import {isThemeSwitchingEnabled} from 'app/utils/theme'; import DisplaySettings from './display_settings'; -import {getAllowedThemes} from 'app/selectors/theme'; function mapStateToProps(state) { const enableTimezone = isTimezoneEnabled(state); @@ -22,4 +24,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(DisplaySettings); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(DisplaySettings); diff --git a/app/screens/settings/general/index.js b/app/screens/settings/general/index.js index ab8457df1..09cc293ab 100644 --- a/app/screens/settings/general/index.js +++ b/app/screens/settings/general/index.js @@ -7,9 +7,10 @@ import {connect} from 'react-redux'; import {clearErrors} from 'mattermost-redux/actions/errors'; import {getCurrentUrl, getConfig} from 'mattermost-redux/selectors/entities/general'; import {getJoinableTeams} from 'mattermost-redux/selectors/entities/teams'; - -import {purgeOfflineStore} from 'app/actions/views/root'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; + +import {goToScreen, dismissModal} from 'app/actions/navigation'; +import {purgeOfflineStore} from 'app/actions/views/root'; import {removeProtocol} from 'app/utils/url'; import Settings from './settings'; @@ -33,6 +34,8 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ clearErrors, purgeOfflineStore, + goToScreen, + dismissModal, }, dispatch), }; } diff --git a/app/screens/settings/general/settings.js b/app/screens/settings/general/settings.js index 732672eb1..94cbdba27 100644 --- a/app/screens/settings/general/settings.js +++ b/app/screens/settings/general/settings.js @@ -11,11 +11,12 @@ import { View, } from 'react-native'; import DeviceInfo from 'react-native-device-info'; +import {Navigation} from 'react-native-navigation'; import SettingsItem from 'app/screens/settings/settings_item'; import StatusBar from 'app/components/status_bar'; import {preventDoubleTap} from 'app/utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import {isValidUrl} from 'app/utils/url'; import {t} from 'app/utils/i18n'; @@ -26,7 +27,10 @@ class Settings extends PureComponent { actions: PropTypes.shape({ clearErrors: PropTypes.func.isRequired, purgeOfflineStore: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string, config: PropTypes.object.isRequired, currentTeamId: PropTypes.string.isRequired, currentUserId: PropTypes.string.isRequired, @@ -34,7 +38,6 @@ class Settings extends PureComponent { errors: PropTypes.array.isRequired, intl: intlShape.isRequired, joinableTeams: PropTypes.array.isRequired, - navigator: PropTypes.object, theme: PropTypes.object, }; @@ -43,9 +46,14 @@ class Settings extends PureComponent { joinableTeams: [], }; - constructor(props) { - super(props); - this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + } + + navigationButtonPressed({buttonId}) { + if (buttonId === 'close-settings') { + this.props.actions.dismissModal(); + } } errorEmailBody = () => { @@ -75,123 +83,60 @@ class Settings extends PureComponent { }; goToAbout = preventDoubleTap(() => { - const {intl, navigator, theme, config} = this.props; - navigator.push({ - screen: 'About', - title: intl.formatMessage({id: 'about.title', defaultMessage: 'About {appTitle}'}, {appTitle: config.SiteName || 'Mattermost'}), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - }, - }); + const {actions, intl, config} = this.props; + const screen = 'About'; + const title = intl.formatMessage({id: 'about.title', defaultMessage: 'About {appTitle}'}, {appTitle: config.SiteName || 'Mattermost'}); + + actions.goToScreen(screen, title); }); goToNotifications = preventDoubleTap(() => { - const {intl, navigator, theme} = this.props; - navigator.push({ - screen: 'NotificationSettings', - backButtonTitle: '', - 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, - }, - }); + const {actions, intl} = this.props; + const screen = 'NotificationSettings'; + const title = intl.formatMessage({id: 'user.settings.modal.notifications', defaultMessage: 'Notifications'}); + + actions.goToScreen(screen, title); }); goToDisplaySettings = preventDoubleTap(() => { - const {intl, navigator, theme} = this.props; - navigator.push({ - screen: 'DisplaySettings', - title: intl.formatMessage({id: 'user.settings.modal.display', defaultMessage: 'Display'}), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }); + const {actions, intl} = this.props; + const screen = 'DisplaySettings'; + const title = intl.formatMessage({id: 'user.settings.modal.display', defaultMessage: 'Display'}); + + actions.goToScreen(screen, title); }); goToAdvancedSettings = preventDoubleTap(() => { - const {intl, navigator, theme} = this.props; - navigator.push({ - screen: 'AdvancedSettings', - title: intl.formatMessage({id: 'mobile.advanced_settings.title', defaultMessage: 'Advanced Settings'}), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }); + const {actions, intl} = this.props; + const screen = 'AdvancedSettings'; + const title = intl.formatMessage({id: 'mobile.advanced_settings.title', defaultMessage: 'Advanced Settings'}); + + actions.goToScreen(screen, title); }); goToSelectTeam = preventDoubleTap(() => { - const {currentUrl, intl, navigator, theme} = this.props; + const {actions, currentUrl, intl, theme} = this.props; + const screen = 'SelectTeam'; + const title = intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}); + const passProps = { + currentUrl, + theme, + }; - navigator.push({ - screen: 'SelectTeam', - title: intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - currentUrl, - theme, - }, - }); + actions.goToScreen(screen, title, passProps); }); goToClientUpgrade = preventDoubleTap(() => { - const {intl, theme} = this.props; + const {actions, intl} = this.props; + const screen = 'ClientUpgrade'; + const title = intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'}); + const passProps = { + userCheckedForUpgrade: true, + }; - this.props.navigator.push({ - screen: 'ClientUpgrade', - title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'}), - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarHidden: false, - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - }, - passProps: { - userCheckedForUpgrade: true, - }, - }); + actions.goToScreen(screen, title, passProps); }); - onNavigatorEvent = (event) => { - if (event.id === 'willAppear') { - setNavigatorStyles(this.props.navigator, this.props.theme); - } - - if (event.type === 'NavBarButtonPress') { - if (event.id === 'close-settings') { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); - } - } - }; - openErrorEmail = preventDoubleTap(() => { const {config} = this.props; const recipient = config.SupportEmail; diff --git a/app/screens/settings/notification_settings/index.js b/app/screens/settings/notification_settings/index.js index 4dd02c312..0196b84d1 100644 --- a/app/screens/settings/notification_settings/index.js +++ b/app/screens/settings/notification_settings/index.js @@ -11,6 +11,8 @@ import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; import {updateMe} from 'mattermost-redux/actions/users'; +import {goToScreen} from 'app/actions/navigation'; + import NotificationSettings from './notification_settings'; function mapStateToProps(state) { @@ -35,6 +37,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ updateMe, + goToScreen, }, dispatch), }; } diff --git a/app/screens/settings/notification_settings/notification_settings.js b/app/screens/settings/notification_settings/notification_settings.js index 4e0ddb351..47bd57e12 100644 --- a/app/screens/settings/notification_settings/notification_settings.js +++ b/app/screens/settings/notification_settings/notification_settings.js @@ -26,10 +26,11 @@ class NotificationSettings extends PureComponent { static propTypes = { actions: PropTypes.shape({ updateMe: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, }), + componentId: PropTypes.string, currentUser: PropTypes.object.isRequired, intl: intlShape.isRequired, - navigator: PropTypes.object, theme: PropTypes.object.isRequired, updateMeRequest: PropTypes.object.isRequired, currentUserStatus: PropTypes.string.isRequired, @@ -38,7 +39,7 @@ class NotificationSettings extends PureComponent { componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } const {updateMeRequest, intl} = nextProps; @@ -61,92 +62,65 @@ class NotificationSettings extends PureComponent { }); goToNotificationSettingsAutoResponder = () => { - const {currentUser, intl, navigator, theme} = this.props; - navigator.push({ - backButtonTitle: '', - screen: 'NotificationSettingsAutoResponder', - title: intl.formatMessage({ - id: 'mobile.notification_settings.auto_responder_short', - defaultMessage: 'Automatic Replies', - }), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - currentUser, - onBack: this.saveAutoResponder, - }, + const {actions, currentUser, intl} = this.props; + const screen = 'NotificationSettingsAutoResponder'; + const title = intl.formatMessage({ + id: 'mobile.notification_settings.auto_responder_short', + defaultMessage: 'Automatic Replies', }); + const passProps = { + currentUser, + onBack: this.saveAutoResponder, + }; + + actions.goToScreen(screen, title, passProps); }; goToNotificationSettingsEmail = () => { - const {currentUser, intl, navigator, theme} = this.props; - navigator.push({ - backButtonTitle: '', - screen: 'NotificationSettingsEmail', - title: intl.formatMessage({ - id: 'mobile.notification_settings.email_title', - defaultMessage: 'Email Notifications', - }), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - currentUser, - }, + const {actions, currentUser, intl} = this.props; + const screen = 'NotificationSettingsEmail'; + const title = intl.formatMessage({ + id: 'mobile.notification_settings.email_title', + defaultMessage: 'Email Notifications', }); + const passProps = { + currentUser, + }; + + actions.goToScreen(screen, title, passProps); }; goToNotificationSettingsMentions = () => { - const {currentUser, intl, navigator, theme} = this.props; - navigator.push({ - backButtonTitle: '', - screen: 'NotificationSettingsMentions', - title: intl.formatMessage({id: 'mobile.notification_settings.mentions_replies', defaultMessage: 'Mentions and Replies'}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - currentUser, - onBack: this.saveNotificationProps, - }, + const {actions, currentUser, intl} = this.props; + const screen = 'NotificationSettingsMentions'; + const title = intl.formatMessage({ + id: 'mobile.notification_settings.mentions_replies', + defaultMessage: 'Mentions and Replies', }); + const passProps = { + currentUser, + onBack: this.saveNotificationProps, + }; + + actions.goToScreen(screen, title, passProps); }; goToNotificationSettingsMobile = () => { - const {currentUser, intl, navigator, theme} = this.props; + const {actions, currentUser, intl} = this.props; + const screen = 'NotificationSettingsMobile'; + const title = intl.formatMessage({ + id: 'mobile.notification_settings.mobile_title', + defaultMessage: 'Mobile Notifications', + }); NotificationPreferences.getPreferences().then((notificationPreferences) => { + const passProps = { + currentUser, + onBack: this.saveNotificationProps, + notificationPreferences, + }; requestAnimationFrame(() => { - navigator.push({ - backButtonTitle: '', - screen: 'NotificationSettingsMobile', - title: intl.formatMessage({id: 'mobile.notification_settings.mobile_title', defaultMessage: 'Mobile Notifications'}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - currentUser, - onBack: this.saveNotificationProps, - notificationPreferences, - }, - }); + actions.goToScreen(screen, title, passProps); }); }).catch((e) => { Alert.alert('There was a problem getting the device preferences', e.message); diff --git a/app/screens/settings/notification_settings_auto_responder/notification_settings_auto_responder.js b/app/screens/settings/notification_settings_auto_responder/notification_settings_auto_responder.js index 1a20646b8..9bf28d12d 100644 --- a/app/screens/settings/notification_settings_auto_responder/notification_settings_auto_responder.js +++ b/app/screens/settings/notification_settings_auto_responder/notification_settings_auto_responder.js @@ -7,9 +7,10 @@ import PropTypes from 'prop-types'; import { View, } from 'react-native'; -import {General} from 'mattermost-redux/constants'; import {intlShape} from 'react-intl'; +import {General} from 'mattermost-redux/constants'; + import FormattedText from 'app/components/formatted_text'; import StatusBar from 'app/components/status_bar'; import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder'; @@ -23,7 +24,6 @@ import SectionItem from 'app/screens/settings/section_item'; export default class NotificationSettingsAutoResponder extends PureComponent { static propTypes = { currentUser: PropTypes.object.isRequired, - navigator: PropTypes.object, onBack: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, currentUserStatus: PropTypes.string.isRequired, @@ -44,8 +44,6 @@ export default class NotificationSettingsAutoResponder extends PureComponent { defaultMessage: 'Hello, I am out of office and unable to respond to messages.', }); - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - let autoResponderActive = 'false'; if (props.currentUserStatus === General.OUT_OF_OFFICE && notifyProps.auto_responder_active) { autoResponderActive = 'true'; @@ -58,15 +56,9 @@ export default class NotificationSettingsAutoResponder extends PureComponent { }; } - onNavigatorEvent = (event) => { - if (event.type === 'ScreenChangedEvent') { - switch (event.id) { - case 'willDisappear': - this.saveUserNotifyProps(); - break; - } - } - }; + componentWillUnmount() { + this.saveUserNotifyProps(); + } saveUserNotifyProps = () => { this.props.onBack({ diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js b/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js index f79b5c26d..dc797176d 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js @@ -6,7 +6,6 @@ import React from 'react'; import Preferences from 'mattermost-redux/constants/preferences'; import {shallowWithIntl} from 'test/intl-test-helper'; -import {emptyFunction} from 'app/utils/general'; import RadioButtonGroup from 'app/components/radio_button'; @@ -23,7 +22,6 @@ describe('NotificationSettingsEmailAndroid', () => { currentUser: {id: 'current_user_id'}, emailInterval: '30', enableEmailBatching: false, - navigator: {setOnNavigatorEvent: emptyFunction}, actions: { updateMe: jest.fn(), savePreferences: jest.fn(), @@ -31,6 +29,7 @@ describe('NotificationSettingsEmailAndroid', () => { sendEmailNotifications: true, siteName: 'Mattermost', theme: Preferences.THEMES.default, + componentId: 'component-id', }; test('should match snapshot', () => { @@ -154,13 +153,15 @@ describe('NotificationSettingsEmailAndroid', () => { const instance = wrapper.instance(); instance.saveEmailNotifyProps = jest.fn(); - // should not save preference on back button on Android + // Back button on Android should close the modal and trigger + // componentDidDisappear. + // Should not save preference on back button on Android as // saving email preference on Android is done via Save button - instance.onNavigatorEvent({type: 'ScreenChangedEvent', id: 'willDisappear'}); + instance.componentDidDisappear(); expect(instance.saveEmailNotifyProps).toHaveBeenCalledTimes(0); wrapper.setState({newInterval: '0'}); - instance.onNavigatorEvent({type: 'ScreenChangedEvent', id: 'willDisappear'}); + instance.componentDidDisappear(); expect(instance.saveEmailNotifyProps).toHaveBeenCalledTimes(0); }); }); diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js b/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js index dcb068ec4..5c5cee410 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js @@ -6,8 +6,6 @@ import {shallow} from 'enzyme'; import Preferences from 'mattermost-redux/constants/preferences'; -import {emptyFunction} from 'app/utils/general'; - import SectionItem from 'app/screens/settings/section_item'; import NotificationSettingsEmailIos from './notification_settings_email.ios.js'; @@ -31,7 +29,6 @@ describe('NotificationSettingsEmailIos', () => { currentUser: {id: 'current_user_id'}, emailInterval: '30', enableEmailBatching: false, - navigator: {setOnNavigatorEvent: emptyFunction}, actions: { updateMe: jest.fn(), savePreferences: jest.fn(), @@ -39,6 +36,7 @@ describe('NotificationSettingsEmailIos', () => { sendEmailNotifications: true, siteName: 'Mattermost', theme: Preferences.THEMES.default, + componentId: 'component-id', }; test('should match snapshot, renderEmailSection', () => { @@ -57,13 +55,13 @@ describe('NotificationSettingsEmailIos', () => { const instance = wrapper.instance(); // should not save preference if email interval has not changed. - instance.onNavigatorEvent({type: 'ScreenChangedEvent', id: 'willDisappear'}); + instance.componentDidDisappear(); expect(baseProps.actions.updateMe).toHaveBeenCalledTimes(0); expect(baseProps.actions.savePreferences).toHaveBeenCalledTimes(0); // should save preference if email interval has changed. wrapper.setState({newInterval: '0'}); - instance.onNavigatorEvent({type: 'ScreenChangedEvent', id: 'willDisappear'}); + instance.componentDidDisappear(); expect(baseProps.actions.updateMe).toHaveBeenCalledTimes(1); expect(baseProps.actions.savePreferences).toHaveBeenCalledTimes(1); }); diff --git a/app/screens/settings/notification_settings_email/notification_settings_email_base.js b/app/screens/settings/notification_settings_email/notification_settings_email_base.js index c63dda1a7..60f56ab8c 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email_base.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email_base.js @@ -4,6 +4,7 @@ import {PureComponent} from 'react'; import {Platform} from 'react-native'; import PropTypes from 'prop-types'; +import {Navigation} from 'react-native-navigation'; import {Preferences} from 'mattermost-redux/constants'; import {getEmailInterval} from 'mattermost-redux/utils/notify_props'; @@ -17,10 +18,10 @@ export default class NotificationSettingsEmailBase extends PureComponent { savePreferences: PropTypes.func.isRequired, updateMe: PropTypes.func.isRequired, }), + componentId: PropTypes.string, currentUser: PropTypes.object.isRequired, emailInterval: PropTypes.string.isRequired, enableEmailBatching: PropTypes.bool.isRequired, - navigator: PropTypes.object, sendEmailNotifications: PropTypes.bool.isRequired, siteName: PropTypes.string, theme: PropTypes.object.isRequired, @@ -33,7 +34,6 @@ export default class NotificationSettingsEmailBase extends PureComponent { currentUser, emailInterval, enableEmailBatching, - navigator, sendEmailNotifications, } = props; @@ -44,13 +44,15 @@ export default class NotificationSettingsEmailBase extends PureComponent { newInterval: this.computeEmailInterval(notifyProps?.email === 'true' && sendEmailNotifications, enableEmailBatching, emailInterval), showEmailNotificationsModal: false, }; + } - navigator.setOnNavigatorEvent(this.onNavigatorEvent); + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); } componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } const { @@ -74,15 +76,11 @@ export default class NotificationSettingsEmailBase extends PureComponent { } } - onNavigatorEvent = (event) => { - if (Platform.OS === 'ios' && event.type === 'ScreenChangedEvent') { - switch (event.id) { - case 'willDisappear': - this.saveEmailNotifyProps(); - break; - } + componentDidDisappear() { + if (Platform.OS === 'ios') { + this.saveEmailNotifyProps(); } - }; + } setEmailInterval = (value) => { this.setState({newInterval: value}); diff --git a/app/screens/settings/notification_settings_mentions/index.js b/app/screens/settings/notification_settings_mentions/index.js index 74b423fde..5a4083b5f 100644 --- a/app/screens/settings/notification_settings_mentions/index.js +++ b/app/screens/settings/notification_settings_mentions/index.js @@ -1,10 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {goToScreen} from 'app/actions/navigation'; + import NotificationSettingsMentions from './notification_settings_mentions'; function mapStateToProps(state) { @@ -13,4 +16,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(NotificationSettingsMentions); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(NotificationSettingsMentions); diff --git a/app/screens/settings/notification_settings_mentions/notification_settings_mention_base.js b/app/screens/settings/notification_settings_mentions/notification_settings_mention_base.js index 79609d5f2..d2fced3eb 100644 --- a/app/screens/settings/notification_settings_mentions/notification_settings_mention_base.js +++ b/app/screens/settings/notification_settings_mentions/notification_settings_mention_base.js @@ -4,15 +4,19 @@ import {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; +import {Navigation} from 'react-native-navigation'; import {getNotificationProps} from 'app/utils/notify_props'; import {setNavigatorStyles} from 'app/utils/theme'; export default class NotificationSettingsMentionsBase extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, + componentId: PropTypes.string, currentUser: PropTypes.object.isRequired, intl: intlShape.isRequired, - navigator: PropTypes.object, onBack: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, }; @@ -27,27 +31,25 @@ export default class NotificationSettingsMentionsBase extends PureComponent { const {currentUser} = props; const notifyProps = getNotificationProps(currentUser); - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - - this.goingBack = true; //use to identify if the navigator is popping this screen + this.goingBack = true; // use to identify if the navigator is popping this screen this.state = this.setStateFromNotifyProps(notifyProps); } + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + } + componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } } - onNavigatorEvent = (event) => { - if (event.type === 'ScreenChangedEvent' && this.goingBack) { - switch (event.id) { - case 'willDisappear': - this.saveUserNotifyProps(); - break; - } + componentDidDisappear() { + if (this.goingBack) { + this.saveUserNotifyProps(); } - }; + } setStateFromNotifyProps = (notifyProps) => { const mentionKeys = (notifyProps.mention_keys || '').split(','); diff --git a/app/screens/settings/notification_settings_mentions/notification_settings_mentions.ios.js b/app/screens/settings/notification_settings_mentions/notification_settings_mentions.ios.js index a82914bb2..681996552 100644 --- a/app/screens/settings/notification_settings_mentions/notification_settings_mentions.ios.js +++ b/app/screens/settings/notification_settings_mentions/notification_settings_mentions.ios.js @@ -20,25 +20,17 @@ import NotificationSettingsMentionsBase from './notification_settings_mention_ba class NotificationSettingsMentionsIos extends NotificationSettingsMentionsBase { goToNotificationSettingsMentionKeywords = () => { - const {intl, navigator, theme} = this.props; + const {actions, intl} = this.props; this.goingBack = false; - navigator.push({ - backButtonTitle: '', - screen: 'NotificationSettingsMentionsKeywords', - title: intl.formatMessage({id: 'mobile.notification_settings_mentions.keywords', defaultMessage: 'Keywords'}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - keywords: this.state.mention_keys, - onBack: this.updateMentionKeys, - }, - }); + const screen = 'NotificationSettingsMentionsKeywords'; + const title = intl.formatMessage({id: 'mobile.notification_settings_mentions.keywords', defaultMessage: 'Keywords'}); + const passProps = { + keywords: this.state.mention_keys, + onBack: this.updateMentionKeys, + }; + + actions.goToScreen(screen, title, passProps); }; renderMentionSection(style) { diff --git a/app/screens/settings/notification_settings_mentions_keywords/index.js b/app/screens/settings/notification_settings_mentions_keywords/index.js index a27f32151..f3a886bb0 100644 --- a/app/screens/settings/notification_settings_mentions_keywords/index.js +++ b/app/screens/settings/notification_settings_mentions_keywords/index.js @@ -1,10 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {popTopScreen} from 'app/actions/navigation'; + import NotificationSettingsMentionsKeywords from './notification_settings_mentions_keywords'; function mapStateToProps(state) { @@ -13,4 +16,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(NotificationSettingsMentionsKeywords); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + popTopScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(NotificationSettingsMentionsKeywords); diff --git a/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.js b/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.js index 4e4f411a1..f6ab36b92 100644 --- a/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.js +++ b/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.js @@ -3,6 +3,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {ScrollView, View} from 'react-native'; +import {Navigation} from 'react-native-navigation'; import FormattedText from 'app/components/formatted_text'; import StatusBar from 'app/components/status_bar'; @@ -11,8 +12,11 @@ import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/ut export default class NotificationSettingsMentionsKeywords extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + popTopScreen: PropTypes.func.isRequired, + }).isRequired, + componentId: PropTypes.string, keywords: PropTypes.string, - navigator: PropTypes.object, onBack: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, }; @@ -23,18 +27,20 @@ export default class NotificationSettingsMentionsKeywords extends PureComponent this.state = { keywords: props.keywords, }; + } - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); } componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } } handleSubmit = () => { - this.props.navigator.pop(); + this.props.actions.popTopScreen(); }; keywordsRef = (ref) => { @@ -45,15 +51,9 @@ export default class NotificationSettingsMentionsKeywords extends PureComponent return this.setState({keywords}); }; - onNavigatorEvent = (event) => { - if (event.type === 'ScreenChangedEvent') { - switch (event.id) { - case 'willDisappear': - this.props.onBack(this.state.keywords); - break; - } - } - }; + componentDidDisappear() { + this.props.onBack(this.state.keywords); + } render() { const {theme} = this.props; diff --git a/app/screens/settings/notification_settings_mobile/notification_settings_mobile_base.js b/app/screens/settings/notification_settings_mobile/notification_settings_mobile_base.js index 80a6bddf7..9eedfc43a 100644 --- a/app/screens/settings/notification_settings_mobile/notification_settings_mobile_base.js +++ b/app/screens/settings/notification_settings_mobile/notification_settings_mobile_base.js @@ -5,6 +5,7 @@ import {PureComponent} from 'react'; import {Platform} from 'react-native'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; +import {Navigation} from 'react-native-navigation'; import {getNotificationProps} from 'app/utils/notify_props'; import {setNavigatorStyles} from 'app/utils/theme'; @@ -14,10 +15,10 @@ export default class NotificationSettingsMobileBase extends PureComponent { actions: PropTypes.shape({ updateMe: PropTypes.func.isRequired, }), + componentId: PropTypes.string, config: PropTypes.object.isRequired, currentUser: PropTypes.object.isRequired, intl: intlShape.isRequired, - navigator: PropTypes.object, notificationPreferences: PropTypes.object, onBack: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, @@ -40,12 +41,15 @@ export default class NotificationSettingsMobileBase extends PureComponent { showMobilePushStatusModal: false, showMobileSoundsModal: false, }; - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); + } + + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); } componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } } @@ -82,15 +86,9 @@ export default class NotificationSettingsMobileBase extends PureComponent { return {}; }; - onNavigatorEvent = (event) => { - if (event.type === 'ScreenChangedEvent') { - switch (event.id) { - case 'willDisappear': - this.saveUserNotifyProps(); - break; - } - } - }; + componentDidDisappear() { + this.saveUserNotifyProps(); + } setMobilePush = (push, callback) => { this.setState({push}, callback); diff --git a/app/screens/sso/index.js b/app/screens/sso/index.js index f17246b5b..64553978a 100644 --- a/app/screens/sso/index.js +++ b/app/screens/sso/index.js @@ -4,6 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; +import {resetToChannel} from 'app/actions/navigation'; import {handleSuccessfulLogin, scheduleExpiredNotification} from 'app/actions/views/login'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; @@ -24,6 +25,7 @@ function mapDispatchToProps(dispatch) { scheduleExpiredNotification, handleSuccessfulLogin, setStoreFromLocalData, + resetToChannel, }, dispatch), }; } diff --git a/app/screens/sso/sso.js b/app/screens/sso/sso.js index 3538d5e73..1ae96be25 100644 --- a/app/screens/sso/sso.js +++ b/app/screens/sso/sso.js @@ -61,7 +61,6 @@ const oneLoginFormScalingJS = ` class SSO extends PureComponent { static propTypes = { intl: intlShape.isRequired, - navigator: PropTypes.object, theme: PropTypes.object, serverUrl: PropTypes.string.isRequired, ssoType: PropTypes.string.isRequired, @@ -69,6 +68,7 @@ class SSO extends PureComponent { scheduleExpiredNotification: PropTypes.func.isRequired, handleSuccessfulLogin: PropTypes.func.isRequired, setStoreFromLocalData: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, }).isRequired, }; @@ -115,25 +115,11 @@ class SSO extends PureComponent { }; goToChannel = () => { - const {navigator} = this.props; tracker.initialLoad = Date.now(); this.scheduleSessionExpiredNotification(); - navigator.resetTo({ - screen: 'Channel', - title: '', - animated: false, - backButtonTitle: '', - navigatorStyle: { - animated: true, - animationType: 'fade', - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - }, - }); + this.props.actions.resetToChannel(); }; onMessage = (event) => { diff --git a/app/screens/terms_of_service/__snapshots__/terms_of_service.test.js.snap b/app/screens/terms_of_service/__snapshots__/terms_of_service.test.js.snap index 81ed4d889..313792907 100644 --- a/app/screens/terms_of_service/__snapshots__/terms_of_service.test.js.snap +++ b/app/screens/terms_of_service/__snapshots__/terms_of_service.test.js.snap @@ -45,100 +45,6 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB disableAtMentions={true} disableChannelLink={true} disableHashtags={true} - navigator={ - Object { - "dismissAllModals": [MockFunction], - "dismissModal": [MockFunction], - "setButtons": [MockFunction] { - "calls": Array [ - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": false, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": false, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "setOnNavigatorEvent": [MockFunction] { - "calls": Array [ - Array [ - [Function], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } textStyles={ Object { "code": Object { @@ -274,123 +180,6 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB disableAtMentions={true} disableChannelLink={true} disableHashtags={true} - navigator={ - Object { - "dismissAllModals": [MockFunction], - "dismissModal": [MockFunction], - "setButtons": [MockFunction] { - "calls": Array [ - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": false, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": false, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "setOnNavigatorEvent": [MockFunction] { - "calls": Array [ - Array [ - [Function], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } textStyles={ Object { "code": Object { @@ -598,100 +387,6 @@ exports[`TermsOfService should match snapshot on enableNavigatorLogout 1`] = ` disableAtMentions={true} disableChannelLink={true} disableHashtags={true} - navigator={ - Object { - "dismissAllModals": [MockFunction], - "dismissModal": [MockFunction], - "setButtons": [MockFunction] { - "calls": Array [ - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": false, - "icon": Object {}, - "id": "close-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "setOnNavigatorEvent": [MockFunction] { - "calls": Array [ - Array [ - [Function], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } textStyles={ Object { "code": Object { diff --git a/app/screens/terms_of_service/index.js b/app/screens/terms_of_service/index.js index a9c843bac..51519d733 100644 --- a/app/screens/terms_of_service/index.js +++ b/app/screens/terms_of_service/index.js @@ -8,6 +8,12 @@ import {getTermsOfService, logout, updateMyTermsOfServiceStatus} from 'mattermos import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import { + setButtons, + dismissModal, + dismissAllModals, +} from 'app/actions/navigation'; + import TermsOfService from './terms_of_service.js'; function mapStateToProps(state) { @@ -25,6 +31,9 @@ function mapDispatchToProps(dispatch) { getTermsOfService, logout, updateMyTermsOfServiceStatus, + setButtons, + dismissModal, + dismissAllModals, }, dispatch), }; } diff --git a/app/screens/terms_of_service/terms_of_service.js b/app/screens/terms_of_service/terms_of_service.js index f6dd2ba13..14fb41968 100644 --- a/app/screens/terms_of_service/terms_of_service.js +++ b/app/screens/terms_of_service/terms_of_service.js @@ -9,6 +9,7 @@ import { View, } from 'react-native'; import {intlShape} from 'react-intl'; +import {Navigation} from 'react-native-navigation'; import FailedNetworkAction from 'app/components/failed_network_action'; import Loading from 'app/components/loading'; @@ -35,9 +36,12 @@ export default class TermsOfService extends PureComponent { logout: PropTypes.func.isRequired, getTermsOfService: PropTypes.func.isRequired, updateMyTermsOfServiceStatus: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + dismissAllModals: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string, closeButton: PropTypes.object, - navigator: PropTypes.object, siteName: PropTypes.string, theme: PropTypes.object, }; @@ -69,63 +73,63 @@ export default class TermsOfService extends PureComponent { termsText: '', }; - this.rightButton.title = context.intl.formatMessage({id: 'terms_of_service.agreeButton', defaultMessage: 'I Agree'}); + this.rightButton.text = context.intl.formatMessage({id: 'terms_of_service.agreeButton', defaultMessage: 'I Agree'}); this.leftButton.icon = props.closeButton; - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); this.setNavigatorButtons(false); } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + this.getTerms(); } componentDidUpdate(prevProps) { if (this.props.theme !== prevProps.theme) { - setNavigatorStyles(this.props.navigator, this.props.theme); + setNavigatorStyles(this.props.componentId, this.props.theme); } } - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - switch (event.id) { - case 'close-terms-of-service': - this.closeTermsAndLogout(); - break; + navigationButtonPressed({buttonId}) { + switch (buttonId) { + case 'close-terms-of-service': + this.closeTermsAndLogout(); + break; - case 'reject-terms-of-service': - this.handleRejectTerms(); - break; + case 'reject-terms-of-service': + this.handleRejectTerms(); + break; - case 'accept-terms-of-service': - this.handleAcceptTerms(); - break; - } + case 'accept-terms-of-service': + this.handleAcceptTerms(); + break; } - }; + } setNavigatorButtons = (enabled = true) => { + const {actions, componentId} = this.props; const buttons = { - leftButtons: [{...this.leftButton, disabled: !enabled}], - rightButtons: [{...this.rightButton, disabled: !enabled}], + leftButtons: [{...this.leftButton, enabled}], + rightButtons: [{...this.rightButton, enabled}], }; - this.props.navigator.setButtons(buttons); + actions.setButtons(componentId, buttons); }; enableNavigatorLogout = () => { const buttons = { - leftButtons: [{...this.leftButton, id: 'close-terms-of-service', disabled: false}], - rightButtons: [{...this.rightButton, disabled: true}], + leftButtons: [{...this.leftButton, id: 'close-terms-of-service', enabled: true}], + rightButtons: [{...this.rightButton, enabled: false}], }; - this.props.navigator.setButtons(buttons); + this.props.actions.setButtons(buttons); }; closeTermsAndLogout = () => { const {actions} = this.props; - this.props.navigator.dismissAllModals(); + actions.dismissAllModals(); actions.logout(); }; @@ -162,9 +166,7 @@ export default class TermsOfService extends PureComponent { this.registerUserAction( true, () => { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); + this.props.actions.dismissModal(); }, this.handleAcceptTerms ); @@ -233,7 +235,7 @@ export default class TermsOfService extends PureComponent { }; render() { - const {navigator, theme} = this.props; + const {theme} = this.props; const styles = getStyleSheet(theme); const blockStyles = getMarkdownBlockStyles(theme); @@ -266,7 +268,6 @@ export default class TermsOfService extends PureComponent { > { getTermsOfService: jest.fn(), updateMyTermsOfServiceStatus: jest.fn(), logout: jest.fn(), + setButtons: jest.fn(), + dismissModal: jest.fn(), + dismissAllModals: jest.fn(), }; const baseProps = { actions, - navigator: { - dismissAllModals: jest.fn(), - dismissModal: jest.fn(), - setButtons: jest.fn(), - setOnNavigatorEvent: jest.fn(), - }, theme: Preferences.THEMES.default, closeButton: {}, siteName: 'Mattermost', + componentId: 'component-id', }; test('should match snapshot', () => { @@ -83,18 +81,18 @@ describe('TermsOfService', () => { expect(wrapper.getElement()).toMatchSnapshot(); }); - test('should call props.navigator.setButtons on setNavigatorButtons', async () => { + test('should call props.actions.setButtons on setNavigatorButtons', async () => { const wrapper = shallow( , {context: {intl: {formatMessage: jest.fn()}}}, ); wrapper.setState({loading: false, termsId: 1, termsText: 'Terms Text'}); - expect(baseProps.navigator.setButtons).toHaveBeenCalledTimes(2); + expect(baseProps.actions.setButtons).toHaveBeenCalledTimes(2); wrapper.instance().setNavigatorButtons(true); - expect(baseProps.navigator.setButtons).toHaveBeenCalledTimes(3); + expect(baseProps.actions.setButtons).toHaveBeenCalledTimes(3); wrapper.instance().setNavigatorButtons(false); - expect(baseProps.navigator.setButtons).toHaveBeenCalledTimes(4); + expect(baseProps.actions.setButtons).toHaveBeenCalledTimes(4); }); test('should enable/disable navigator buttons on setNavigatorButtons true/false', () => { @@ -129,6 +127,6 @@ describe('TermsOfService', () => { wrapper.setState({loading: false, termsId: 1, termsText: 'Terms Text'}); wrapper.instance().closeTermsAndLogout(); - expect(baseProps.navigator.dismissAllModals).toHaveBeenCalledTimes(1); + expect(baseProps.actions.dismissAllModals).toHaveBeenCalledTimes(1); }); }); diff --git a/app/screens/text_preview/text_preview.js b/app/screens/text_preview/text_preview.js index 532da4efa..daa637fa7 100644 --- a/app/screens/text_preview/text_preview.js +++ b/app/screens/text_preview/text_preview.js @@ -17,14 +17,14 @@ import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/ut export default class TextPreview extends React.PureComponent { static propTypes = { - navigator: PropTypes.object.isRequired, + componentId: PropTypes.string, theme: PropTypes.object.isRequired, content: PropTypes.string.isRequired, }; componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } } diff --git a/app/screens/theme/theme.js b/app/screens/theme/theme.js index a2a37d43f..0fed665d1 100644 --- a/app/screens/theme/theme.js +++ b/app/screens/theme/theme.js @@ -27,11 +27,11 @@ export default class Theme extends React.PureComponent { actions: PropTypes.shape({ savePreferences: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string, allowedThemes: PropTypes.arrayOf(PropTypes.object), customTheme: PropTypes.object, isLandscape: PropTypes.bool.isRequired, isTablet: PropTypes.bool.isRequired, - navigator: PropTypes.object.isRequired, teamId: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, userId: PropTypes.string.isRequired, @@ -56,7 +56,7 @@ export default class Theme extends React.PureComponent { componentDidUpdate(prevProps) { if (prevProps.theme !== this.props.theme) { - setNavigatorStyles(this.props.navigator, this.props.theme); + setNavigatorStyles(this.props.componentId, this.props.theme); } } diff --git a/app/screens/theme/theme.test.js b/app/screens/theme/theme.test.js index 3a2651ba5..486b751fe 100644 --- a/app/screens/theme/theme.test.js +++ b/app/screens/theme/theme.test.js @@ -131,9 +131,6 @@ describe('Theme', () => { allowedThemes, isLandscape: false, isTablet: false, - navigator: { - setOnNavigatorEvent: jest.fn(), - }, teamId: 'test-team', theme: Preferences.THEMES.default, userId: 'test-user', diff --git a/app/screens/thread/__snapshots__/thread.ios.test.js.snap b/app/screens/thread/__snapshots__/thread.ios.test.js.snap index ebef67766..897c3640e 100644 --- a/app/screens/thread/__snapshots__/thread.ios.test.js.snap +++ b/app/screens/thread/__snapshots__/thread.ios.test.js.snap @@ -13,28 +13,6 @@ exports[`thread should match snapshot, has root post 1`] = ` lastPostIndex={2} lastViewedAt={0} location="thread" - navigator={ - Object { - "dismissModal": [MockFunction], - "pop": [MockFunction], - "resetTo": [MockFunction], - "setTitle": [MockFunction] { - "calls": Array [ - Array [ - Object { - "title": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } onPostPress={[Function]} postIds={ Array [ @@ -76,28 +54,6 @@ exports[`thread should match snapshot, has root post 1`] = ` channelId="channel_id" channelIsArchived={false} cursorPositionEvent="onThreadTextBoxCursorChange" - navigator={ - Object { - "dismissModal": [MockFunction], - "pop": [MockFunction], - "resetTo": [MockFunction], - "setTitle": [MockFunction] { - "calls": Array [ - Array [ - Object { - "title": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } onCloseChannel={[Function]} rootId="root_id" valueEvent="onThreadTextBoxValueChange" @@ -128,28 +84,6 @@ exports[`thread should match snapshot, render footer 1`] = ` lastPostIndex={2} lastViewedAt={0} location="thread" - navigator={ - Object { - "dismissModal": [MockFunction], - "pop": [MockFunction], - "resetTo": [MockFunction], - "setTitle": [MockFunction] { - "calls": Array [ - Array [ - Object { - "title": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } onPostPress={[Function]} postIds={ Array [ @@ -176,28 +110,6 @@ exports[`thread should match snapshot, render footer 2`] = ` lastPostIndex={2} lastViewedAt={0} location="thread" - navigator={ - Object { - "dismissModal": [MockFunction], - "pop": [MockFunction], - "resetTo": [MockFunction], - "setTitle": [MockFunction] { - "calls": Array [ - Array [ - Object { - "title": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } onPostPress={[Function]} postIds={ Array [ diff --git a/app/screens/thread/index.js b/app/screens/thread/index.js index 7f1573ab0..a089d7755 100644 --- a/app/screens/thread/index.js +++ b/app/screens/thread/index.js @@ -10,6 +10,8 @@ import {selectPost} from 'mattermost-redux/actions/posts'; import {makeGetChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; import {makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts'; +import {popTopScreen, resetToChannel} from 'app/actions/navigation'; + import Thread from './thread'; function makeMapStateToProps() { @@ -37,6 +39,8 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ selectPost, + popTopScreen, + resetToChannel, }, dispatch), }; } diff --git a/app/screens/thread/thread.android.js b/app/screens/thread/thread.android.js index 2184e0d88..849c3a2fa 100644 --- a/app/screens/thread/thread.android.js +++ b/app/screens/thread/thread.android.js @@ -19,7 +19,6 @@ export default class ThreadAndroid extends ThreadBase { const { channelId, myMember, - navigator, postIds, rootId, channelIsArchived, @@ -36,7 +35,6 @@ export default class ThreadAndroid extends ThreadBase { currentUserId={myMember && myMember.user_id} lastViewedAt={this.state.lastViewedAt} lastPostIndex={-1} - navigator={navigator} onPostPress={this.hideKeyboard} location={THREAD} /> @@ -47,7 +45,6 @@ export default class ThreadAndroid extends ThreadBase { channelIsArchived={channelIsArchived} rootId={rootId} channelId={channelId} - navigator={navigator} onCloseChannel={this.onCloseChannel} /> ); diff --git a/app/screens/thread/thread.ios.js b/app/screens/thread/thread.ios.js index 7ba12e368..863717d26 100644 --- a/app/screens/thread/thread.ios.js +++ b/app/screens/thread/thread.ios.js @@ -29,7 +29,6 @@ export default class ThreadIOS extends ThreadBase { const { channelId, myMember, - navigator, postIds, rootId, channelIsArchived, @@ -47,7 +46,6 @@ export default class ThreadIOS extends ThreadBase { lastPostIndex={getLastPostIndex(postIds)} currentUserId={myMember && myMember.user_id} lastViewedAt={this.state.lastViewedAt} - navigator={navigator} onPostPress={this.hideKeyboard} location={THREAD} scrollViewNativeID={SCROLLVIEW_NATIVE_ID} @@ -77,7 +75,6 @@ export default class ThreadIOS extends ThreadBase { channelIsArchived={channelIsArchived} rootId={rootId} channelId={channelId} - navigator={navigator} onCloseChannel={this.onCloseChannel} cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE} valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE} diff --git a/app/screens/thread/thread.ios.test.js b/app/screens/thread/thread.ios.test.js index 309b71088..0478ed22d 100644 --- a/app/screens/thread/thread.ios.test.js +++ b/app/screens/thread/thread.ios.test.js @@ -11,22 +11,22 @@ import PostList from 'app/components/post_list'; import ThreadIOS from './thread.ios'; jest.mock('react-intl'); +jest.mock('react-native-navigation', () => ({ + Navigation: { + mergeOptions: jest.fn(), + }, +})); describe('thread', () => { - const navigator = { - dismissModal: jest.fn(), - pop: jest.fn(), - resetTo: jest.fn(), - setTitle: jest.fn(), - }; const baseProps = { actions: { selectPost: jest.fn(), + popTopScreen: jest.fn(), + resetToChannel: jest.fn(), }, channelId: 'channel_id', channelType: General.OPEN_CHANNEL, displayName: 'channel_display_name', - navigator, myMember: {last_viewed_at: 0, user_id: 'member_user_id'}, rootId: 'root_id', theme: Preferences.THEMES.default, @@ -55,35 +55,19 @@ describe('thread', () => { expect(wrapper.getElement()).toMatchSnapshot(); }); - test('should call props.navigator on onCloseChannel', () => { - const channelScreen = { - screen: 'Channel', - title: '', - animated: false, - backButtonTitle: '', - navigatorStyle: { - animated: true, - animationType: 'fade', - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - }, - passProps: { - disableTermsModal: true, - }, + test('should call props.actions.resetToChannel on onCloseChannel', () => { + const passProps = { + disableTermsModal: true, }; - const newNavigator = {...navigator}; const wrapper = shallow( , {context: {intl: {formatMessage: jest.fn()}}}, ); wrapper.instance().onCloseChannel(); - expect(newNavigator.resetTo).toHaveBeenCalledTimes(1); - expect(newNavigator.resetTo).toBeCalledWith(channelScreen); + expect(baseProps.actions.resetToChannel).toHaveBeenCalledTimes(1); + expect(baseProps.actions.resetToChannel).toBeCalledWith(passProps); }); test('should match snapshot, render footer', () => { diff --git a/app/screens/thread/thread_base.js b/app/screens/thread/thread_base.js index 95718d020..0b64e7e32 100644 --- a/app/screens/thread/thread_base.js +++ b/app/screens/thread/thread_base.js @@ -3,8 +3,9 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Keyboard, Platform} from 'react-native'; +import {Keyboard} from 'react-native'; import {intlShape} from 'react-intl'; +import {Navigation} from 'react-native-navigation'; import {General, RequestStatus} from 'mattermost-redux/constants'; @@ -16,11 +17,13 @@ export default class ThreadBase extends PureComponent { static propTypes = { actions: PropTypes.shape({ selectPost: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string, channelId: PropTypes.string.isRequired, channelType: PropTypes.string, displayName: PropTypes.string, - navigator: PropTypes.object, myMember: PropTypes.object.isRequired, rootId: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, @@ -52,8 +55,12 @@ export default class ThreadBase extends PureComponent { this.postTextbox = React.createRef(); - this.props.navigator.setTitle({ - title, + Navigation.mergeOptions(props.componentId, { + topBar: { + title: { + text: title, + }, + }, }); this.state = { @@ -63,7 +70,7 @@ export default class ThreadBase extends PureComponent { componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } if (this.props.postIds !== nextProps.postIds && !nextProps.postIds.length) { @@ -81,17 +88,7 @@ export default class ThreadBase extends PureComponent { } close = () => { - const {navigator} = this.props; - - if (Platform.OS === 'ios') { - navigator.pop({ - animated: true, - }); - } else { - navigator.dismissModal({ - animationType: 'slide-down', - }); - } + this.props.actions.popTopScreen(); }; handleAutoComplete = (value) => { @@ -123,22 +120,9 @@ export default class ThreadBase extends PureComponent { }; onCloseChannel = () => { - this.props.navigator.resetTo({ - screen: 'Channel', - title: '', - animated: false, - backButtonTitle: '', - navigatorStyle: { - animated: true, - animationType: 'fade', - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - }, - passProps: { - disableTermsModal: true, - }, - }); + const passProps = { + disableTermsModal: true, + }; + this.props.actions.resetToChannel(passProps); }; } diff --git a/app/screens/timezone/index.js b/app/screens/timezone/index.js index fb2901c89..8b015bc5a 100644 --- a/app/screens/timezone/index.js +++ b/app/screens/timezone/index.js @@ -10,6 +10,7 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getUserTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {goToScreen} from 'app/actions/navigation'; import {updateUser} from 'app/actions/views/edit_profile'; import Timezone from './timezone'; @@ -32,6 +33,7 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ getSupportedTimezones, updateUser, + goToScreen, }, dispatch), }; } diff --git a/app/screens/timezone/select_timezone/index.js b/app/screens/timezone/select_timezone/index.js index 5fb95760f..4def5ad3e 100644 --- a/app/screens/timezone/select_timezone/index.js +++ b/app/screens/timezone/select_timezone/index.js @@ -1,11 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getSupportedTimezones} from 'mattermost-redux/selectors/entities/general'; +import {popTopScreen} from 'app/actions/navigation'; + import SelectTimezone from './select_timezone'; function mapStateToProps(state, props) { @@ -26,4 +29,12 @@ function mapStateToProps(state, props) { }; } -export default connect(mapStateToProps)(SelectTimezone); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + popTopScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(SelectTimezone); diff --git a/app/screens/timezone/select_timezone/select_timezone.js b/app/screens/timezone/select_timezone/select_timezone.js index f52559010..79f3a20ce 100644 --- a/app/screens/timezone/select_timezone/select_timezone.js +++ b/app/screens/timezone/select_timezone/select_timezone.js @@ -23,10 +23,12 @@ const VIEWABILITY_CONFIG = ListTypes.VISIBILITY_CONFIG_DEFAULTS; export default class Timezone extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + popTopScreen: PropTypes.func.isRequired, + }).isRequired, selectedTimezone: PropTypes.string.isRequired, initialScrollIndex: PropTypes.number.isRequired, timezones: PropTypes.array.isRequired, - navigator: PropTypes.object, onBack: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, }; @@ -59,7 +61,7 @@ export default class Timezone extends PureComponent { timezoneSelected = (timezone) => { this.props.onBack(timezone); - this.props.navigator.pop(); + this.props.actions.popTopScreen(); }; handleTextChanged = (value) => { diff --git a/app/screens/timezone/timezone.js b/app/screens/timezone/timezone.js index 52ff47116..1badd938e 100644 --- a/app/screens/timezone/timezone.js +++ b/app/screens/timezone/timezone.js @@ -21,7 +21,6 @@ import {getDeviceTimezone} from 'app/utils/timezone'; export default class Timezone extends PureComponent { static propTypes = { - navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, timezones: PropTypes.array.isRequired, user: PropTypes.object.isRequired, @@ -116,29 +115,20 @@ export default class Timezone extends PureComponent { goToSelectTimezone = () => { const { + actions, userTimezone: {manualTimezone}, - navigator, - theme, } = this.props; const {intl} = this.context; + const screen = 'SelectTimezone'; + const title = intl.formatMessage({id: 'mobile.timezone_settings.select', defaultMessage: 'Select Timezone'}); + const passProps = { + selectedTimezone: manualTimezone, + onBack: this.updateManualTimezone, + }; + this.goingBack = false; - navigator.push({ - backButtonTitle: '', - screen: 'SelectTimezone', - title: intl.formatMessage({id: 'mobile.timezone_settings.select', defaultMessage: 'Select Timezone'}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - selectedTimezone: manualTimezone, - onBack: this.updateManualTimezone, - }, - }); + actions.goToScreen(screen, title, passProps); }; render() { diff --git a/app/screens/user_profile/index.js b/app/screens/user_profile/index.js index adeb16750..f4e926c61 100644 --- a/app/screens/user_profile/index.js +++ b/app/screens/user_profile/index.js @@ -15,6 +15,13 @@ import {loadBot} from 'mattermost-redux/actions/bots'; import {getBotAccounts} from 'mattermost-redux/selectors/entities/bots'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import { + setButtons, + dismissModal, + resetToChannel, + goToScreen, +} from 'app/actions/navigation'; + import UserProfile from './user_profile'; function mapStateToProps(state, ownProps) { @@ -43,6 +50,10 @@ function mapDispatchToProps(dispatch) { makeDirectChannel, setChannelDisplayName, loadBot, + setButtons, + dismissModal, + resetToChannel, + goToScreen, }, dispatch), }; } diff --git a/app/screens/user_profile/user_profile.js b/app/screens/user_profile/user_profile.js index f1bf1ba34..848e8332d 100644 --- a/app/screens/user_profile/user_profile.js +++ b/app/screens/user_profile/user_profile.js @@ -10,6 +10,7 @@ import { Linking, } from 'react-native'; import {intlShape} from 'react-intl'; +import {Navigation} from 'react-native-navigation'; import {displayUsername} from 'mattermost-redux/utils/user_utils'; import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; @@ -32,10 +33,14 @@ export default class UserProfile extends PureComponent { makeDirectChannel: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, loadBot: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string, config: PropTypes.object.isRequired, currentDisplayName: PropTypes.string, - navigator: PropTypes.object, teammateNameDisplay: PropTypes.string, theme: PropTypes.object.isRequired, user: PropTypes.object.isRequired, @@ -59,54 +64,53 @@ export default class UserProfile extends PureComponent { super(props); if (props.isMyUser) { - this.rightButton.title = context.intl.formatMessage({id: 'mobile.routes.user_profile.edit', defaultMessage: 'Edit'}); + this.rightButton.text = context.intl.formatMessage({id: 'mobile.routes.user_profile.edit', defaultMessage: 'Edit'}); const buttons = { rightButtons: [this.rightButton], }; - props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - props.navigator.setButtons(buttons); + props.actions.setButtons(props.componentId, buttons); } } componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { - setNavigatorStyles(this.props.navigator, nextProps.theme); + setNavigatorStyles(this.props.componentId, nextProps.theme); } } componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + if (this.props.user && this.props.user.is_bot) { this.props.actions.loadBot(this.props.user.id); } } - close = () => { - const {navigator, theme} = this.props; + navigationButtonPressed({buttonId}) { + switch (buttonId) { + case this.rightButton.id: + this.goToEditProfile(); + break; + case 'close-settings': + this.close(); + break; + } + } - if (this.props.fromSettings) { - navigator.dismissModal({ - animationType: 'slide-down', - }); + close = () => { + const {actions, fromSettings} = this.props; + + if (fromSettings) { + actions.dismissModal(); return; } - navigator.resetTo({ - screen: 'Channel', - animated: true, - navigatorStyle: { - animated: true, - animationType: 'fade', - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - disableTermsModal: true, - }, - }); + const passProps = { + disableTermsModal: true, + }; + actions.resetToChannel(passProps); }; getDisplayName = () => { @@ -217,43 +221,18 @@ export default class UserProfile extends PureComponent { }; goToEditProfile = () => { - const {user: currentUser} = this.props; + const {actions, user: currentUser} = this.props; const {formatMessage} = this.context.intl; const commandType = 'Push'; - - const {navigator, theme} = this.props; - const options = { - screen: 'EditProfile', - title: formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}), - animated: true, - backButtonTitle: '', - passProps: {currentUser, commandType}, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }; + const screen = 'EditProfile'; + const title = formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}); + const passProps = {currentUser, commandType}; requestAnimationFrame(() => { - navigator.push(options); + actions.goToScreen(screen, title, passProps); }); }; - onNavigatorEvent = (event) => { - if (event.type === 'NavBarButtonPress') { - switch (event.id) { - case this.rightButton.id: - this.goToEditProfile(); - break; - case 'close-settings': - this.close(); - break; - } - } - }; - renderAdditionalOptions = () => { if (!Config.ExperimentalProfileLinks) { return null; diff --git a/app/screens/user_profile/user_profile.test.js b/app/screens/user_profile/user_profile.test.js index e1dac798e..5c425058f 100644 --- a/app/screens/user_profile/user_profile.test.js +++ b/app/screens/user_profile/user_profile.test.js @@ -22,6 +22,10 @@ describe('user_profile', () => { setChannelDisplayName: jest.fn(), makeDirectChannel: jest.fn(), loadBot: jest.fn(), + setButtons: jest.fn(), + dismissModal: jest.fn(), + resetToChannel: jest.fn(), + goToScreen: jest.fn(), }; const baseProps = { actions, @@ -29,16 +33,12 @@ describe('user_profile', () => { ShowEmailAddress: true, }, teammateNameDisplay: 'username', - navigator: { - resetTo: jest.fn(), - push: jest.fn(), - dismissModal: jest.fn(), - }, teams: [], theme: Preferences.THEMES.default, enableTimezone: false, militaryTime: false, isMyUser: false, + componentId: 'component-id', }; const user = { @@ -89,16 +89,9 @@ describe('user_profile', () => { }); test('should push EditProfile', async () => { - const props = { - ...baseProps, - navigator: { - push: jest.fn(), - }, - }; - const wrapper = shallow( , {context: {intl: {formatMessage: jest.fn()}}}, @@ -106,18 +99,18 @@ describe('user_profile', () => { wrapper.instance().goToEditProfile(); setTimeout(() => { - expect(props.navigator.push).toHaveBeenCalledTimes(1); + expect(baseProps.actions.goToScreen).toHaveBeenCalledTimes(1); }, 16); }); test('should call goToEditProfile', () => { const props = { ...baseProps, - navigator: { - push: jest.fn(), + actions: { + ...baseProps.actions, + goToScreen: jest.fn(), }, }; - const wrapper = shallow( { {context: {intl: {formatMessage: jest.fn()}}}, ); - const event = {type: 'NavBarButtonPress', id: wrapper.instance().rightButton.id}; - wrapper.instance().onNavigatorEvent(event); + const event = {buttonId: wrapper.instance().rightButton.id}; + wrapper.instance().navigationButtonPressed(event); setTimeout(() => { - expect(props.navigator.push).toHaveBeenCalledTimes(1); + expect(baseProps.actions.goToScreen).toHaveBeenCalledTimes(1); }, 0); }); @@ -144,13 +137,13 @@ describe('user_profile', () => { {context: {intl: {formatMessage: jest.fn()}}}, ); - const event = {type: 'NavBarButtonPress', id: 'close-settings'}; - wrapper.instance().onNavigatorEvent(event); - expect(props.navigator.dismissModal).toHaveBeenCalledTimes(1); + const event = {buttonId: 'close-settings'}; + wrapper.instance().navigationButtonPressed(event); + expect(props.actions.dismissModal).toHaveBeenCalledTimes(1); props.fromSettings = false; wrapper.setProps({...props}); - wrapper.instance().onNavigatorEvent(event); - expect(props.navigator.resetTo).toHaveBeenCalledTimes(1); + wrapper.instance().navigationButtonPressed(event); + expect(props.actions.resetToChannel).toHaveBeenCalledTimes(1); }); }); diff --git a/app/store/ephemeral_store.js b/app/store/ephemeral_store.js index dad1dac11..f95803088 100644 --- a/app/store/ephemeral_store.js +++ b/app/store/ephemeral_store.js @@ -6,6 +6,19 @@ class EphemeralStore { this.appStarted = false; this.appStartedFromPushNotification = false; this.deviceToken = null; + this.componentIdStack = []; + } + + getTopComponentId = () => this.componentIdStack[0]; + + addComponentIdToStack = (componentId) => { + this.componentIdStack.unshift(componentId); + } + + removeComponentIdFromStack = (componentId) => { + this.componentIdStack = this.componentIdStack.filter((id) => { + return id !== componentId; + }); } } diff --git a/app/utils/images.js b/app/utils/images.js index 2d0ab6771..c231e2ca2 100644 --- a/app/utils/images.js +++ b/app/utils/images.js @@ -57,44 +57,27 @@ export const calculateDimensions = (height, width, viewPortWidth = 0, viewPortHe }; }; -export function previewImageAtIndex(navigator, components, index, files) { +export function previewImageAtIndex(components, index, files, showModalOverCurrentContext) { previewComponents = components; const component = components[index]; if (component) { component.measure((rx, ry, width, height, x, y) => { - goToImagePreview( - navigator, - { + Keyboard.dismiss(); + requestAnimationFrame(() => { + const screen = 'ImagePreview'; + const passProps = { index, origin: {x, y, width, height}, target: {x: 0, y: 0, opacity: 1}, files, getItemMeasures, - } - ); + }; + showModalOverCurrentContext(screen, passProps); + }); }); } } -function goToImagePreview(navigator, passProps) { - Keyboard.dismiss(); - requestAnimationFrame(() => { - navigator.showModal({ - screen: 'ImagePreview', - title: '', - animationType: 'none', - passProps, - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - modalPresentationStyle: 'overCurrentContext', - }, - }); - }); -} - function getItemMeasures(index, cb) { const activeComponent = previewComponents[index]; diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js index b23a5d528..0214bbdd8 100644 --- a/app/utils/push_notifications.js +++ b/app/utils/push_notifications.js @@ -15,12 +15,13 @@ import { createPostForNotificationReply, loadFromPushNotification, } from 'app/actions/views/root'; +import {dismissAllModals, popToRoot} from 'app/actions/navigation'; import {ViewTypes} from 'app/constants'; import {getLocalizedMessage} from 'app/i18n'; import {getCurrentServerUrl, getAppCredentials} from 'app/init/credentials'; import PushNotifications from 'app/push_notifications'; import {getCurrentLocale} from 'app/selectors/i18n'; -import ephemeralStore from 'app/store/ephemeral_store'; +import EphemeralStore from 'app/store/ephemeral_store'; import {t} from 'app/utils/i18n'; class PushNotificationUtils { @@ -44,8 +45,14 @@ class PushNotificationUtils { loadFromNotification = async (notification) => { await this.store.dispatch(loadFromPushNotification(notification, true)); - if (!ephemeralStore.appStartedFromPushNotification) { - EventEmitter.emit(ViewTypes.NOTIFICATION_TAPPED); + if (!EphemeralStore.appStartedFromPushNotification) { + EventEmitter.emit('close_channel_drawer'); + EventEmitter.emit('close_settings_sidebar'); + + const {dispatch} = this.store; + dispatch(dismissAllModals()); + dispatch(popToRoot()); + PushNotifications.resetNotification(); } }; @@ -171,13 +178,13 @@ class PushNotificationUtils { prefix = General.PUSH_NOTIFY_ANDROID_REACT_NATIVE; } - ephemeralStore.deviceToken = `${prefix}:${data.token}`; + EphemeralStore.deviceToken = `${prefix}:${data.token}`; // TODO: Remove when realm is ready const waitForHydration = () => { if (getState().views.root.hydrationComplete && !this.configured) { this.configured = true; - dispatch(setDeviceToken(ephemeralStore.deviceToken)); + dispatch(setDeviceToken(EphemeralStore.deviceToken)); unsubscribeFromStore(); } }; diff --git a/app/utils/theme.js b/app/utils/theme.js index f307f6398..c8c751371 100644 --- a/app/utils/theme.js +++ b/app/utils/theme.js @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import {StyleSheet} from 'react-native'; +import {Navigation} from 'react-native-navigation'; import * as ThemeUtils from 'mattermost-redux/utils/theme_utils'; @@ -19,12 +20,21 @@ export function concatStyles(...styles) { return [].concat(styles); } -export function setNavigatorStyles(navigator, theme) { - navigator.setStyle({ - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, +export function setNavigatorStyles(componentId, theme) { + Navigation.mergeOptions(componentId, { + topBar: { + title: { + color: theme.sidebarHeaderTextColor, + }, + background: { + color: theme.sidebarHeaderBg, + }, + leftButtonColor: theme.sidebarHeaderTextColor, + rightButtonColor: theme.sidebarHeaderTextColor, + }, + layout: { + backgroundColor: theme.centerChannelBg, + }, }); } diff --git a/app/utils/wrap_context_provider.js b/app/utils/wrap_context_provider.js index 015d9378a..cea58cabb 100644 --- a/app/utils/wrap_context_provider.js +++ b/app/utils/wrap_context_provider.js @@ -6,10 +6,8 @@ import IntlWrapper from 'app/components/root'; export function wrapWithContextProvider(Comp, excludeEvents = true) { return (props) => { //eslint-disable-line react/display-name - const {navigator} = props; //eslint-disable-line react/prop-types return ( diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 642ba4619..eab0ca226 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -183,7 +183,7 @@ lane :build do |options| build end -desc 'Buid the app for Android and iOS unsigned' +desc 'Build the app for Android and iOS unsigned' lane :unsigned do configure @@ -507,7 +507,7 @@ platform :android do config_mode = ENV['BUILD_FOR_RELEASE'] == 'true' ? 'Release' : 'Debug' gradle( - task: 'assemble', + task: 'app:assemble', build_type: config_mode, project_dir: 'android/' ) diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 8e9c2ae17..48a94901a 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -93,7 +93,6 @@ 7FABE04622137F5C00D0F595 /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; }; 7FABE0562213884700D0F595 /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; }; 7FBB5E9B1E1F5A4B000DE18A /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; }; - 7FC200E81EBB65370099331B /* libReactNativeNavigation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FC200DF1EBB65100099331B /* libReactNativeNavigation.a */; }; 7FDB92B11F706F58006CDFD1 /* libRNImagePicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDB92A71F706F45006CDFD1 /* libRNImagePicker.a */; }; 7FEB10981F6101710039A015 /* BlurAppScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB10971F6101710039A015 /* BlurAppScreen.m */; }; 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; @@ -102,6 +101,7 @@ 7FF31C1421330B7900680B75 /* libRNFetchBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FF31C1321330B4200680B75 /* libRNFetchBlob.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 84E3264B229834C30055068A /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E325FF229834C30055068A /* Config.swift */; }; + 84596ECE229D93FD00981086 /* libReactNativeNavigation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 84596EB1229D93B600981086 /* libReactNativeNavigation.a */; }; 895C9A56B94A45C1BAF568FE /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AC6EB561E1F64C17A69D2FAD /* Entypo.ttf */; }; 8D26455C994F46C39B1392F2 /* libRNSafeArea.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9263CF9B16054263B13EA23B /* libRNSafeArea.a */; }; 9358B95F95184EE0A4DCE629 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */; }; @@ -629,13 +629,6 @@ remoteGlobalIDString = 7FABE03622137F2900D0F595; remoteInfo = UploadAttachments; }; - 7FC200DE1EBB65100099331B /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7FC200BC1EBB65100099331B /* ReactNativeNavigation.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = D8AFADBD1BEE6F3F00A4592D; - remoteInfo = ReactNativeNavigation; - }; 7FDB92A61F706F45006CDFD1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */; @@ -699,6 +692,20 @@ remoteGlobalIDString = 58B5119B1A9E6C1200147676; remoteInfo = RCTText; }; + 84596EB0229D93B600981086 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 84596E7B229D93B600981086 /* ReactNativeNavigation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D8AFADBD1BEE6F3F00A4592D; + remoteInfo = ReactNativeNavigation; + }; + 84596EB2229D93B600981086 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 84596E7B229D93B600981086 /* ReactNativeNavigation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 7B49FEBB1E95090800DEB3EA; + remoteInfo = ReactNativeNavigationTests; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -834,7 +841,6 @@ 7FABDFC12211A39000D0F595 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; 7FABE0092212650600D0F595 /* ChannelsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsViewController.swift; sourceTree = ""; }; 7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = UploadAttachments.xcodeproj; path = UploadAttachments/UploadAttachments.xcodeproj; sourceTree = ""; }; - 7FC200BC1EBB65100099331B /* ReactNativeNavigation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeNavigation.xcodeproj; path = "../node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj"; sourceTree = ""; }; 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNImagePicker.xcodeproj; path = "../node_modules/react-native-image-picker/ios/RNImagePicker.xcodeproj"; sourceTree = ""; }; 7FEB10961F6101710039A015 /* BlurAppScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BlurAppScreen.h; path = Mattermost/BlurAppScreen.h; sourceTree = ""; }; 7FEB10971F6101710039A015 /* BlurAppScreen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BlurAppScreen.m; path = Mattermost/BlurAppScreen.m; sourceTree = ""; }; @@ -856,6 +862,8 @@ 7FFE32BE1FD9CCAA0038C7A0 /* SDWebImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SDWebImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7FFE32BF1FD9CCAA0038C7A0 /* Sentry.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Sentry.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; + 84596DD7229C853000981086 /* libReactNativeNavigation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReactNativeNavigation.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 84596E7B229D93B600981086 /* ReactNativeNavigation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeNavigation.xcodeproj; path = "../node_modules/react-native-navigation/lib/ios/ReactNativeNavigation.xcodeproj"; sourceTree = ""; }; 849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSafeArea.xcodeproj; path = "../node_modules/react-native-safe-area/ios/RNSafeArea.xcodeproj"; sourceTree = ""; }; 84E325FF229834C30055068A /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 8F0B22D2C9924FAFA7FB681C /* Roboto-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-LightItalic.ttf"; path = "../assets/fonts/Roboto-LightItalic.ttf"; sourceTree = ""; }; @@ -923,7 +931,7 @@ 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, 7F43D63F1F6BFA19001FC614 /* libBVLinearGradient.a in Frameworks */, - 7FC200E81EBB65370099331B /* libReactNativeNavigation.a in Frameworks */, + 84596ECE229D93FD00981086 /* libReactNativeNavigation.a in Frameworks */, 7F1A56B4227E38B600EF7A90 /* libRNCAsyncStorage.a in Frameworks */, 7F4C2598227E3B11009144EF /* libRNCNetInfo.a in Frameworks */, 7F2691A11EE1DC6A007574FE /* libRNNotifications.a in Frameworks */, @@ -1217,6 +1225,7 @@ 65FD5EA57EBAE06106094B2F /* libPods-Mattermost.a */, 4246DC09024BB33A3E491E25 /* libPods-MattermostTests.a */, CF19152887874B7E996210B1 /* libz.tbd */, + 84596DD7229C853000981086 /* libReactNativeNavigation.a */, ); name = Frameworks; sourceTree = ""; @@ -1449,14 +1458,6 @@ name = Products; sourceTree = ""; }; - 7FC200BD1EBB65100099331B /* Products */ = { - isa = PBXGroup; - children = ( - 7FC200DF1EBB65100099331B /* libReactNativeNavigation.a */, - ); - name = Products; - sourceTree = ""; - }; 7FDB92761F706F45006CDFD1 /* Products */ = { isa = PBXGroup; children = ( @@ -1510,6 +1511,7 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + 84596E7B229D93B600981086 /* ReactNativeNavigation.xcodeproj */, 7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */, 7F11AA06228848D8001C9540 /* KeyboardTrackingView.xcodeproj */, 7F1A5663227E389600EF7A90 /* RNCAsyncStorage.xcodeproj */, @@ -1525,7 +1527,6 @@ 37ABD3971F4CE13B001FDE6B /* ART.xcodeproj */, 3752184A1F4B9E980035444B /* RCTCameraRoll.xcodeproj */, 7F26919B1EE1DC51007574FE /* RNNotifications.xcodeproj */, - 7FC200BC1EBB65100099331B /* ReactNativeNavigation.xcodeproj */, 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */, 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */, @@ -1594,6 +1595,15 @@ name = Products; sourceTree = ""; }; + 84596E7C229D93B600981086 /* Products */ = { + isa = PBXGroup; + children = ( + 84596EB1229D93B600981086 /* libReactNativeNavigation.a */, + 84596EB3229D93B600981086 /* ReactNativeNavigationTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1829,8 +1839,8 @@ ProjectRef = 3B1E210BF3B649BBBF427977 /* ReactNativeExceptionHandler.xcodeproj */; }, { - ProductGroup = 7FC200BD1EBB65100099331B /* Products */; - ProjectRef = 7FC200BC1EBB65100099331B /* ReactNativeNavigation.xcodeproj */; + ProductGroup = 84596E7C229D93B600981086 /* Products */; + ProjectRef = 84596E7B229D93B600981086 /* ReactNativeNavigation.xcodeproj */; }, { ProductGroup = 7F7D7F53201645D300D31155 /* Products */; @@ -2393,13 +2403,6 @@ remoteRef = 7FABE04422137F2A00D0F595 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 7FC200DF1EBB65100099331B /* libReactNativeNavigation.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libReactNativeNavigation.a; - remoteRef = 7FC200DE1EBB65100099331B /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; 7FDB92A71F706F45006CDFD1 /* libRNImagePicker.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -2456,6 +2459,20 @@ remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 84596EB1229D93B600981086 /* libReactNativeNavigation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReactNativeNavigation.a; + remoteRef = 84596EB0229D93B600981086 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 84596EB3229D93B600981086 /* ReactNativeNavigationTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = ReactNativeNavigationTests.xctest; + remoteRef = 84596EB2229D93B600981086 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -2774,7 +2791,7 @@ ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**", - "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", + "$(SRCROOT)/../node_modules/react-native-navigation/lib/ios/**", "$(SRCROOT)/../node_modules/react-native-notifications/RNNotifications/**", "$(SRCROOT)/../node_modules/react-native-local-auth", "$(SRCROOT)/../node_modules/react-native-passcode-status/ios", @@ -2834,7 +2851,7 @@ ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**", - "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", + "$(SRCROOT)/../node_modules/react-native-navigation/lib/ios/**", "$(SRCROOT)/../node_modules/react-native-notifications/RNNotifications/**", "$(SRCROOT)/../node_modules/react-native-local-auth", "$(SRCROOT)/../node_modules/react-native-passcode-status/ios", diff --git a/ios/Mattermost/AppDelegate.m b/ios/Mattermost/AppDelegate.m index 1e3ff9097..33cce9067 100644 --- a/ios/Mattermost/AppDelegate.m +++ b/ios/Mattermost/AppDelegate.m @@ -17,7 +17,7 @@ #else #import "RNSentry.h" // This is used for versions of react < 0.40 #endif -#import "RCCManager.h" +#import #import "RNNotifications.h" #import #import @@ -53,13 +53,9 @@ NSString* const NotificationClearAction = @"clear"; [[NSUserDefaults standardUserDefaults] synchronize]; } - NSURL *jsCodeLocation; + NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; + [ReactNativeNavigation bootstrap:jsCodeLocation launchOptions:launchOptions]; - jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; - - self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - self.window.backgroundColor = [UIColor whiteColor]; - [[RCCManager sharedInstance] initBridgeWithBundleURL:jsCodeLocation launchOptions:launchOptions]; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error: nil]; os_log(OS_LOG_DEFAULT, "Mattermost started!!"); diff --git a/package-lock.json b/package-lock.json index 40c5261e4..c88461969 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5795,6 +5795,11 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepmerge": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.2.1.tgz", + "integrity": "sha512-+hbDSzTqEW0fWgnlKksg7XAOtT+ddZS5lHZJ6f6MdixRs9wQy+50fm1uUCVb1IkvjLUYX/SfFO021ZNwriURTw==" + }, "default-require-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", @@ -15550,10 +15555,30 @@ "from": "github:mattermost/react-native-local-auth#cc9ce2f468fbf7b431dfad3191a31aaa9227a6ab" }, "react-native-navigation": { - "version": "github:migbot/react-native-navigation#03c623c373f818827a463ca0fe90f86f56e71b2f", - "from": "github:migbot/react-native-navigation#03c623c373f818827a463ca0fe90f86f56e71b2f", + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-2.21.1.tgz", + "integrity": "sha512-CcICsn02NhfUZtTEg5QnXn7MiAlewc+uA3Q+UBO3SZw2rN94j8FBmEhvbvhFSWecBzqUTRvsXueMFVo4ruTAcQ==", "requires": { - "lodash": "4.x.x" + "hoist-non-react-statics": "3.x.x", + "lodash": "4.17.x", + "prop-types": "15.x.x", + "react-lifecycles-compat": "2.0.0", + "tslib": "1.9.3" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz", + "integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==", + "requires": { + "react-is": "^16.7.0" + } + }, + "react-lifecycles-compat": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-2.0.0.tgz", + "integrity": "sha512-txfpPCQYiazVdcbMRhatqWKcAxJweUu2wDXvts5/7Wyp6+Y9cHojqXHsLPEckzutfHlxZhG8Oiundbmp8Fd6eQ==" + } } }, "react-native-notifications": { diff --git a/package.json b/package.json index 07060dd0d..43f288f57 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "commonmark": "github:mattermost/commonmark.js#4224e725f14b5d4e340a62d7e56d5ad451e9f8cf", "commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#3a2ac19cab725ad28b170fdc1d397dddedcf87eb", "deep-equal": "1.0.1", + "deepmerge": "3.2.1", "emoji-regex": "8.0.0", "fuse.js": "3.4.4", "intl": "1.2.5", @@ -44,7 +45,7 @@ "react-native-keychain": "3.1.3", "react-native-linear-gradient": "2.5.4", "react-native-local-auth": "github:mattermost/react-native-local-auth#cc9ce2f468fbf7b431dfad3191a31aaa9227a6ab", - "react-native-navigation": "github:migbot/react-native-navigation#03c623c373f818827a463ca0fe90f86f56e71b2f", + "react-native-navigation": "2.21.1", "react-native-notifications": "github:mattermost/react-native-notifications#1b7ec8513606b42237ab4674de9dacb4d1935e38", "react-native-passcode-status": "1.1.1", "react-native-permissions": "1.1.1", diff --git a/packager/README.md b/packager/README.md index 3df9d58f1..dd03ad15c 100644 --- a/packager/README.md +++ b/packager/README.md @@ -27,6 +27,7 @@ - remotedev-utils - socketcluster-client - stacktrace-parser + - react-navigation 8. Change development versions of certain files to production ones: - configureStore.dev.js -> configureStore.prod.js - react/cjs/react.development.js -> react/cjs/react.production.min.js diff --git a/packager/moduleNames.js b/packager/moduleNames.js index 7e79e8e76..17572aadf 100644 --- a/packager/moduleNames.js +++ b/packager/moduleNames.js @@ -382,16 +382,28 @@ module.exports = [ 'node_modules/react-native-keychain/index.js', 'node_modules/react-native-local-auth/LocalAuth.android.js', 'node_modules/react-native-local-auth/index.js', - 'node_modules/react-native-navigation/src/NativeEventsReceiver.js', - 'node_modules/react-native-navigation/src/Navigation.js', - 'node_modules/react-native-navigation/src/PropRegistry.js', - 'node_modules/react-native-navigation/src/Screen.js', - 'node_modules/react-native-navigation/src/ScreenVisibilityListener.js', - 'node_modules/react-native-navigation/src/deprecated/indexDeprecated.android.js', - 'node_modules/react-native-navigation/src/deprecated/platformSpecificDeprecated.android.js', - 'node_modules/react-native-navigation/src/index.js', - 'node_modules/react-native-navigation/src/platformSpecific.android.js', - 'node_modules/react-native-navigation/src/views/sharedElementTransition.android.js', + 'node_modules/react-native-navigation/lib/dist/Navigation.js', + 'node_modules/react-native-navigation/lib/dist/adapters/AppRegistryService.js', + 'node_modules/react-native-navigation/lib/dist/adapters/AssetResolver.js', + 'node_modules/react-native-navigation/lib/dist/adapters/ColorService.js', + 'node_modules/react-native-navigation/lib/dist/adapters/Constants.js', + 'node_modules/react-native-navigation/lib/dist/adapters/NativeCommandsSender.js', + 'node_modules/react-native-navigation/lib/dist/adapters/NativeEventsReceiver.js', + 'node_modules/react-native-navigation/lib/dist/adapters/SharedElement.js', + 'node_modules/react-native-navigation/lib/dist/adapters/TouchablePreview.js', + 'node_modules/react-native-navigation/lib/dist/adapters/UniqueIdProvider.js', + 'node_modules/react-native-navigation/lib/dist/commands/Commands.js', + 'node_modules/react-native-navigation/lib/dist/commands/LayoutTreeCrawler.js', + 'node_modules/react-native-navigation/lib/dist/commands/LayoutTreeParser.js', + 'node_modules/react-native-navigation/lib/dist/commands/OptionsProcessor.js', + 'node_modules/react-native-navigation/lib/dist/components/ComponentRegistry.js', + 'node_modules/react-native-navigation/lib/dist/components/ComponentWrapper.js', + 'node_modules/react-native-navigation/lib/dist/components/Store.js', + 'node_modules/react-native-navigation/lib/dist/events/CommandsObserver.js', + 'node_modules/react-native-navigation/lib/dist/events/ComponentEventsObserver.js', + 'node_modules/react-native-navigation/lib/dist/events/EventsRegistry.js', + 'node_modules/react-native-navigation/lib/dist/index.js', + 'node_modules/react-native-navigation/lib/dist/interfaces/Options.js', 'node_modules/react-native-notifications/index.android.js', 'node_modules/react-native-notifications/notification.android.js', 'node_modules/react-native/Libraries/Animated/src/Animated.js', @@ -604,4 +616,4 @@ module.exports = [ 'node_modules/symbol-observable/lib/index.js', 'node_modules/symbol-observable/lib/ponyfill.js', 'node_modules/url-parse/index.js', -]; +]; \ No newline at end of file diff --git a/packager/modulePaths.js b/packager/modulePaths.js index cef5a4ff2..084158aa9 100644 --- a/packager/modulePaths.js +++ b/packager/modulePaths.js @@ -380,16 +380,28 @@ module.exports = ['./node_modules/app/components/loading.js', './node_modules/node_modules/react-native-keychain/index.js', './node_modules/node_modules/react-native-local-auth/LocalAuth.android.js', './node_modules/node_modules/react-native-local-auth/index.js', - './node_modules/node_modules/react-native-navigation/src/NativeEventsReceiver.js', - './node_modules/node_modules/react-native-navigation/src/Navigation.js', - './node_modules/node_modules/react-native-navigation/src/PropRegistry.js', - './node_modules/node_modules/react-native-navigation/src/Screen.js', - './node_modules/node_modules/react-native-navigation/src/ScreenVisibilityListener.js', - './node_modules/node_modules/react-native-navigation/src/deprecated/indexDeprecated.android.js', - './node_modules/node_modules/react-native-navigation/src/deprecated/platformSpecificDeprecated.android.js', - './node_modules/node_modules/react-native-navigation/src/index.js', - './node_modules/node_modules/react-native-navigation/src/platformSpecific.android.js', - './node_modules/node_modules/react-native-navigation/src/views/sharedElementTransition.android.js', + './node_modules/node_modules/react-native-navigation/lib/dist/Navigation.js', + './node_modules/node_modules/react-native-navigation/lib/dist/adapters/AppRegistryService.js', + './node_modules/node_modules/react-native-navigation/lib/dist/adapters/AssetResolver.js', + './node_modules/node_modules/react-native-navigation/lib/dist/adapters/ColorService.js', + './node_modules/node_modules/react-native-navigation/lib/dist/adapters/Constants.js', + './node_modules/node_modules/react-native-navigation/lib/dist/adapters/NativeCommandsSender.js', + './node_modules/node_modules/react-native-navigation/lib/dist/adapters/NativeEventsReceiver.js', + './node_modules/node_modules/react-native-navigation/lib/dist/adapters/SharedElement.js', + './node_modules/node_modules/react-native-navigation/lib/dist/adapters/TouchablePreview.js', + './node_modules/node_modules/react-native-navigation/lib/dist/adapters/UniqueIdProvider.js', + './node_modules/node_modules/react-native-navigation/lib/dist/commands/Commands.js', + './node_modules/node_modules/react-native-navigation/lib/dist/commands/LayoutTreeCrawler.js', + './node_modules/node_modules/react-native-navigation/lib/dist/commands/LayoutTreeParser.js', + './node_modules/node_modules/react-native-navigation/lib/dist/commands/OptionsProcessor.js', + './node_modules/node_modules/react-native-navigation/lib/dist/components/ComponentRegistry.js', + './node_modules/node_modules/react-native-navigation/lib/dist/components/ComponentWrapper.js', + './node_modules/node_modules/react-native-navigation/lib/dist/components/Store.js', + './node_modules/node_modules/react-native-navigation/lib/dist/events/CommandsObserver.js', + './node_modules/node_modules/react-native-navigation/lib/dist/events/ComponentEventsObserver.js', + './node_modules/node_modules/react-native-navigation/lib/dist/events/EventsRegistry.js', + './node_modules/node_modules/react-native-navigation/lib/dist/index.js', + './node_modules/node_modules/react-native-navigation/lib/dist/interfaces/Options.js', './node_modules/node_modules/react-native-notifications/index.android.js', './node_modules/node_modules/react-native-notifications/notification.android.js', './node_modules/node_modules/react-native/Libraries/Animated/src/Animated.js',