From e8712f9199b8f8e337adae3391fd354f9ef83e85 Mon Sep 17 00:00:00 2001 From: Chris Duarte Date: Fri, 18 May 2018 14:13:00 -0700 Subject: [PATCH] Cold startup js refactor (#1598) * Switch to SingleDex and remove all locales * WIP Mattermost Start Component for lazy loading modules * Add files changed for native modules * Add Entry component and app global object * dispatch setStatusBarHeight for iOS * Update screen imports * Include Entry screen * Refactor app to mattermost.android.js * Override unnecessary java files * Fix minor issues in changes * Display empty state based on user credentials Also, add proper background theme for empty loading screen * Add native module constant cache support * Fix startup theme regression * Add Keychain support for credentials * Fix Orientation regression * Fix SharedExtension regression * Emit NATIVE_APP_LAUNCHED across bridge only once during cold start * Add iOS Support * Revert to previous implementation of i18n * Fix styling issues * Include listener for SERVER_VERSION_CHANGED * Add SafeAreaView in Entry screen * Register deviceToken early, in order to get iOS PN Support * Include StartTimeModule * Add ReplyFromPush support and remove NATIVE_APP_LAUNCHED listener * Package native constants in StartTimeModule and avoid bridge calls * Fix check-style errors * Code cleanup * Rename StartTimeModule to InitializationModule * Remove NavigationApplication * Documentation and minor changes * Account for app opening after SharedExtension * Refactor getIntl to getTranslations * Move native module constants into it's own forked repos * Include FetchBlob and DeviceInfo forked repos --- Makefile | 2 + android/app/build.gradle | 2 +- .../rnbeta/InitializationModule.java | 154 +++ .../rnbeta/InitializationPackage.java | 36 + .../mattermost/rnbeta/MainApplication.java | 14 +- .../rnbeta/NotificationReplyService.java | 2 + .../AsyncStorageHelper.java | 93 ++ .../KeysReadableArray.java | 68 ++ .../ResolvePromise.java | 38 + .../com/mattermost/share/ShareActivity.java | 9 +- .../com/mattermost/share/ShareModule.java | 25 +- .../com/mattermost/share/SharePackage.java | 9 +- android/settings.gradle | 2 + app/actions/views/root.js | 10 + app/app.js | 290 ++++++ app/components/start/empty_toolbar.js | 96 ++ app/components/status_bar/status_bar.js | 6 +- app/constants/view.js | 3 + app/mattermost.js | 976 +++++++----------- .../push_notifications.android.js | 2 +- app/screens/entry/entry.js | 274 +++++ app/screens/entry/index.js | 36 + app/screens/index.js | 135 +-- app/utils/avoid_native_bridge.js | 19 + app/utils/error_handling.js | 62 ++ app/utils/push_notifications.js | 157 +++ index.js | 2 - ios/Mattermost.xcodeproj/project.pbxproj | 47 + package-lock.json | 11 +- package.json | 5 +- 30 files changed, 1883 insertions(+), 702 deletions(-) create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/InitializationModule.java create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/InitializationPackage.java create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/AsyncStorageHelper.java create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/KeysReadableArray.java create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/ResolvePromise.java create mode 100644 app/app.js create mode 100644 app/components/start/empty_toolbar.js create mode 100644 app/screens/entry/entry.js create mode 100644 app/screens/entry/index.js create mode 100644 app/utils/avoid_native_bridge.js create mode 100644 app/utils/error_handling.js create mode 100644 app/utils/push_notifications.js diff --git a/Makefile b/Makefile index 8f85ed563..ce855579b 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,7 @@ post-install: @cp ./native_modules/ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker @# Need to copy custom RNDocumentPicker.m that implements direct access to the document picker in iOS @cp ./native_modules/RNDocumentPicker.m node_modules/react-native-document-picker/ios/RNDocumentPicker/RNDocumentPicker.m + @rm -f node_modules/intl/.babelrc @# Hack to get react-intl and its dependencies to work with react-native @# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js @@ -78,6 +79,7 @@ post-install: @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|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_FULL_USER);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java + @sed -i'' -e "s|activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);|activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);|g" node_modules/react-native-orientation/android/src/main/java/com/github/yamill/orientation/OrientationModule.java @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 @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; \ diff --git a/android/app/build.gradle b/android/app/build.gradle index ff0bd2940..ca9accf69 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -113,7 +113,6 @@ android { targetSdkVersion 23 versionCode 103 versionName "1.8.0" - multiDexEnabled true ndk { abiFilters "armeabi-v7a", "x86" } @@ -169,6 +168,7 @@ android { dependencies { compile project(':react-native-document-picker') + compile project(':react-native-keychain') compile project(':react-native-doc-viewer') compile project(':react-native-video') compile fileTree(dir: "libs", include: ["*.jar"]) diff --git a/android/app/src/main/java/com/mattermost/rnbeta/InitializationModule.java b/android/app/src/main/java/com/mattermost/rnbeta/InitializationModule.java new file mode 100644 index 000000000..a8b655fd0 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/InitializationModule.java @@ -0,0 +1,154 @@ +package com.mattermost.rnbeta; + +import android.app.Application; +import android.support.annotation.Nullable; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.mattermost.rnbeta.react_native_interface.AsyncStorageHelper; +import com.mattermost.rnbeta.react_native_interface.KeysReadableArray; +import com.mattermost.rnbeta.react_native_interface.ResolvePromise; +import com.oblador.keychain.KeychainModule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class InitializationModule extends ReactContextBaseJavaModule { + + static final String TOOLBAR_BACKGROUND = "TOOLBAR_BACKGROUND"; + static final String TOOLBAR_TEXT_COLOR = "TOOLBAR_TEXT_COLOR"; + static final String APP_BACKGROUND = "APP_BACKGROUND"; + static final String DEVICE_SECURE_CACHE_KEY = "DEVICE_SECURE_CACHE_KEY"; + + private final Application mApplication; + + public InitializationModule(Application application, ReactApplicationContext reactContext) { + super(reactContext); + mApplication = application; + } + + @Override + public String getName() { + return "Initialization"; + } + + @Nullable + @Override + public Map getConstants() { + Map constants = new HashMap<>(); + + /** + * Package all native module variables in constants + * in order to avoid the native bridge + * + * KeyStore: + * credentialsExist + * deviceToken + * currentUserId + * token + * url + * + * AsyncStorage: + * toolbarBackground + * toolbarTextColor + * appBackground + * isDeviceSecure + * + * Miscellaneous: + * MattermostManaged.Config + * replyFromPushNotification + */ + + MainApplication app = (MainApplication) mApplication; + final Boolean[] credentialsExist = {false}; + final WritableMap[] credentials = {null}; + final Object[] config = {null}; + + // Get KeyStore credentials + KeychainModule module = new KeychainModule(this.getReactApplicationContext()); + module.getGenericPasswordForOptions(null, new ResolvePromise() { + @Override + public void resolve(@Nullable Object value) { + if (value instanceof Boolean && !(Boolean)value) { + credentialsExist[0] = false; + return; + } + + WritableMap map = (WritableMap) value; + if (map != null) { + credentialsExist[0] = true; + credentials[0] = map; + } + } + }); + + // Get managedConfig from MattermostManagedModule + MattermostManagedModule.getInstance().getConfig(new ResolvePromise() { + @Override + public void resolve(@Nullable Object value) { + WritableNativeMap nativeMap = (WritableNativeMap) value; + config[0] = value; + } + }); + + + // Get AsyncStorage key/values + final ArrayList keys = new ArrayList(5); + keys.add(TOOLBAR_BACKGROUND); + keys.add(TOOLBAR_TEXT_COLOR); + keys.add(APP_BACKGROUND); + keys.add(DEVICE_SECURE_CACHE_KEY); + KeysReadableArray asyncStorageKeys = new KeysReadableArray() { + @Override + public int size() { + return keys.size(); + } + + @Override + public String getString(int index) { + return keys.get(index); + } + }; + + AsyncStorageHelper asyncStorage = new AsyncStorageHelper(this.getReactApplicationContext()); + HashMap asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys); + + String toolbarBackground = asyncStorageResults.get(TOOLBAR_BACKGROUND); + String toolbarTextColor = asyncStorageResults.get(TOOLBAR_TEXT_COLOR); + String appBackground = asyncStorageResults.get(APP_BACKGROUND); + String managedConfigResult = asyncStorageResults.get(DEVICE_SECURE_CACHE_KEY); + + if (toolbarBackground != null + && toolbarTextColor != null + && appBackground != null) { + + constants.put("themesExist", true); + constants.put("toolbarBackground", toolbarBackground); + constants.put("toolbarTextColor", toolbarTextColor); + constants.put("appBackground", appBackground); + } else { + constants.put("themesExist", false); + } + + if (managedConfigResult != null) { + constants.put("managedConfigResult", managedConfigResult); + } + + + if (credentialsExist[0]) { + constants.put("credentialsExist", true); + constants.put("credentials", credentials[0]); + } else { + constants.put("credentialsExist", false); + } + + constants.put("managedConfig", config[0]); + constants.put("replyFromPushNotification", app.replyFromPushNotification); + app.replyFromPushNotification = false; + + return constants; + } +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/InitializationPackage.java b/android/app/src/main/java/com/mattermost/rnbeta/InitializationPackage.java new file mode 100644 index 000000000..79e7e12e7 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/InitializationPackage.java @@ -0,0 +1,36 @@ +package com.mattermost.rnbeta; + +import android.app.Application; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class InitializationPackage implements ReactPackage { + + private final Application mApplication; + + public InitializationPackage(Application application) { + mApplication = application; + } + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new InitializationModule(mApplication, reactContext)); + } + + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} 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 c85123980..ef6f01b2f 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -1,13 +1,13 @@ package com.mattermost.rnbeta; import com.mattermost.share.SharePackage; -import android.app.Application; + import android.support.annotation.NonNull; import android.content.Context; import android.os.Bundle; -import com.facebook.react.ReactApplication; import com.reactnativedocumentpicker.ReactNativeDocumentPicker; +import com.oblador.keychain.KeychainPackage; import com.reactlibrary.RNReactNativeDocViewerPackage; import com.brentvatne.react.ReactVideoPackage; import com.horcrux.svg.SvgPackage; @@ -17,10 +17,8 @@ import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage; import com.RNFetchBlob.RNFetchBlobPackage; import com.gantix.JailMonkey.JailMonkeyPackage; import io.tradle.react.LocalAuthPackage; -import com.facebook.react.ReactInstanceManager; -import com.facebook.react.ReactNativeHost; + import com.facebook.react.ReactPackage; -import com.facebook.react.shell.MainReactPackage; import com.facebook.soloader.SoLoader; import com.imagepicker.ImagePickerPackage; @@ -43,6 +41,8 @@ import java.util.List; public class MainApplication extends NavigationApplication implements INotificationsApplication { public NotificationsLifecycleFacade notificationsLifecycleFacade; + public Boolean sharedExtensionIsOpened = false; + public Boolean replyFromPushNotification = false; @Override public boolean isDebug() { @@ -74,7 +74,9 @@ public class MainApplication extends NavigationApplication implements INotificat new ReactVideoPackage(), new RNReactNativeDocViewerPackage(), new ReactNativeDocumentPicker(), - new SharePackage() + new SharePackage(this), + new KeychainPackage(), + new InitializationPackage(this) ); } diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java index 1376168a7..40d61a805 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java @@ -32,6 +32,8 @@ public class NotificationReplyService extends HeadlessJsTaskService { int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1); CustomPushNotification.clearNotification(mContext, notificationId, channelId); + MainApplication app = (MainApplication) this.getApplication(); + app.replyFromPushNotification = true; Log.i("ReactNative", "Replying service"); return new HeadlessJsTaskConfig( "notificationReplied", diff --git a/android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/AsyncStorageHelper.java b/android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/AsyncStorageHelper.java new file mode 100644 index 000000000..1f33057f9 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/AsyncStorageHelper.java @@ -0,0 +1,93 @@ +package com.mattermost.rnbeta.react_native_interface; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.modules.storage.ReactDatabaseSupplier; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; + +/** + * AsyncStorageHelper: Class that accesses React Native AsyncStorage Database synchronously + */ +public class AsyncStorageHelper { + + // Static variables from: com.facebook.react.modules.storage.ReactDatabaseSupplier + static final String TABLE_CATALYST = "catalystLocalStorage"; + static final String KEY_COLUMN = "key"; + static final String VALUE_COLUMN = "value"; + + + private static final int MAX_SQL_KEYS = 999; + + Context mReactContext = null; + + public AsyncStorageHelper(Context mReactContext) { + this.mReactContext = mReactContext; + } + + public HashMap multiGet(ReadableArray keys) { + HashMap results = new HashMap<>(keys.size()); + + HashSet keysRemaining = new HashSet<>(); + String[] columns = {KEY_COLUMN, VALUE_COLUMN}; + ReactDatabaseSupplier reactDatabaseSupplier = ReactDatabaseSupplier.getInstance(this.mReactContext); + for (int keyStart = 0; keyStart < keys.size(); keyStart += MAX_SQL_KEYS) { + int keyCount = Math.min(keys.size() - keyStart, MAX_SQL_KEYS); + Cursor cursor = reactDatabaseSupplier.get().query( + TABLE_CATALYST, + columns, + buildKeySelection(keyCount), + buildKeySelectionArgs(keys, keyStart, keyCount), + null, + null, + null); + keysRemaining.clear(); + + try { + if (cursor.getCount() != keys.size()) { + // some keys have not been found - insert them with null into the final array + for (int keyIndex = keyStart; keyIndex < keyStart + keyCount; keyIndex++) { + keysRemaining.add(keys.getString(keyIndex)); + } + } + + if (cursor.moveToFirst()) { + do { + results.put(cursor.getString(0), cursor.getString(1)); + keysRemaining.remove(cursor.getString(0)); + } while (cursor.moveToNext()); + } + } catch (Exception e) { + return new HashMap<>(1); + } finally { + cursor.close(); + } + + for (String key : keysRemaining) { + results.put(key, null); + } + keysRemaining.clear(); + } + + return results; + } + + private static String buildKeySelection(int selectionCount) { + String[] list = new String[selectionCount]; + Arrays.fill(list, "?"); + return KEY_COLUMN + " IN (" + TextUtils.join(", ", list) + ")"; + } + + private static String[] buildKeySelectionArgs(ReadableArray keys, int start, int count) { + String[] selectionArgs = new String[count]; + for (int keyIndex = 0; keyIndex < count; keyIndex++) { + selectionArgs[keyIndex] = keys.getString(start + keyIndex); + } + return selectionArgs; + } +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/KeysReadableArray.java b/android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/KeysReadableArray.java new file mode 100644 index 000000000..352c04199 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/KeysReadableArray.java @@ -0,0 +1,68 @@ +package com.mattermost.rnbeta.react_native_interface; + +import com.facebook.react.bridge.Dynamic; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; + +import java.util.ArrayList; + +/** + * KeysReadableArray: Helper class that abstracts boilerplate + */ +public class KeysReadableArray implements ReadableArray { + @Override + public int size() { + return 0; + } + + @Override + public boolean isNull(int index) { + return false; + } + + @Override + public boolean getBoolean(int index) { + return false; + } + + @Override + public double getDouble(int index) { + return 0; + } + + @Override + public int getInt(int index) { + return 0; + } + + @Override + public String getString(int index) { + return null; + } + + @Override + public ReadableArray getArray(int index) { + return null; + } + + @Override + public ReadableMap getMap(int index) { + return null; + } + + @Override + public Dynamic getDynamic(int index) { + return null; + } + + @Override + public ReadableType getType(int index) { + return null; + } + + @Override + public ArrayList toArrayList() { + return null; + } +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/ResolvePromise.java b/android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/ResolvePromise.java new file mode 100644 index 000000000..f7491efd8 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/react_native_interface/ResolvePromise.java @@ -0,0 +1,38 @@ +package com.mattermost.rnbeta.react_native_interface; + +import com.facebook.react.bridge.Promise; + +/** + * ResolvePromise: Helper class that abstracts boilerplate + */ +public class ResolvePromise implements Promise { + @Override + public void resolve(@javax.annotation.Nullable Object value) { + + } + + @Override + public void reject(String code, String message) { + + } + + @Override + public void reject(String code, Throwable e) { + + } + + @Override + public void reject(String code, String message, Throwable e) { + + } + + @Override + public void reject(String message) { + + } + + @Override + public void reject(Throwable reason) { + + } +} diff --git a/android/app/src/main/java/com/mattermost/share/ShareActivity.java b/android/app/src/main/java/com/mattermost/share/ShareActivity.java index b15a9ad97..573f9ac83 100644 --- a/android/app/src/main/java/com/mattermost/share/ShareActivity.java +++ b/android/app/src/main/java/com/mattermost/share/ShareActivity.java @@ -1,13 +1,20 @@ package com.mattermost.share; -import android.content.Intent; import android.os.Bundle; import com.facebook.react.ReactActivity; +import com.mattermost.rnbeta.MainApplication; public class ShareActivity extends ReactActivity { @Override protected String getMainComponentName() { return "MattermostShare"; } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + MainApplication app = (MainApplication) this.getApplication(); + app.sharedExtensionIsOpened = true; + } } diff --git a/android/app/src/main/java/com/mattermost/share/ShareModule.java b/android/app/src/main/java/com/mattermost/share/ShareModule.java index 63ebe8b8d..5ce9cd654 100644 --- a/android/app/src/main/java/com/mattermost/share/ShareModule.java +++ b/android/app/src/main/java/com/mattermost/share/ShareModule.java @@ -2,7 +2,6 @@ package com.mattermost.share; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; @@ -10,23 +9,25 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.Arguments; +import com.mattermost.rnbeta.MainApplication; import android.app.Activity; import android.content.Intent; import android.net.Uri; -import android.util.Log; -import android.graphics.Bitmap; -import java.io.InputStream; import java.io.File; import java.util.ArrayList; -import javax.annotation.Nonnull; + +import javax.annotation.Nullable; import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONException; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -37,8 +38,11 @@ import okhttp3.Response; public class ShareModule extends ReactContextBaseJavaModule { private final OkHttpClient client = new OkHttpClient(); public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - public ShareModule(ReactApplicationContext reactContext) { + private final MainApplication mApplication; + + public ShareModule(MainApplication application, ReactApplicationContext reactContext) { super(reactContext); + mApplication = application; } private File tempFolder; @@ -59,6 +63,15 @@ public class ShareModule extends ReactContextBaseJavaModule { } } + @Nullable + @Override + public Map getConstants() { + HashMap constants = new HashMap<>(1); + constants.put("isOpened", mApplication.sharedExtensionIsOpened); + mApplication.sharedExtensionIsOpened = false; + return constants; + } + @ReactMethod public void close(ReadableMap data) { this.clear(); diff --git a/android/app/src/main/java/com/mattermost/share/SharePackage.java b/android/app/src/main/java/com/mattermost/share/SharePackage.java index a1c745f96..f10810a41 100644 --- a/android/app/src/main/java/com/mattermost/share/SharePackage.java +++ b/android/app/src/main/java/com/mattermost/share/SharePackage.java @@ -5,15 +5,22 @@ import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.NativeModule; import com.facebook.react.uimanager.ViewManager; import com.facebook.react.ReactPackage; +import com.mattermost.rnbeta.MainApplication; import java.util.Arrays; import java.util.Collections; import java.util.List; public class SharePackage implements ReactPackage { + MainApplication mApplication; + + public SharePackage(MainApplication application) { + mApplication = application; + } + @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new ShareModule(reactContext)); + return Arrays.asList(new ShareModule(mApplication, reactContext)); } public List> createJSModules() { diff --git a/android/settings.gradle b/android/settings.gradle index 830921b9f..154166adf 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,6 +1,8 @@ rootProject.name = 'Mattermost' include ':react-native-document-picker' project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android') +include ':react-native-keychain' +project(':react-native-keychain').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keychain/android') include ':react-native-doc-viewer' project(':react-native-doc-viewer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-doc-viewer/android') include ':react-native-video' diff --git a/app/actions/views/root.js b/app/actions/views/root.js index 6dccf4be8..608d9d897 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -9,6 +9,7 @@ import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'matterm import {getPosts} from 'mattermost-redux/actions/posts'; import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams'; +import {ViewTypes} from 'app/constants'; import {recordTime} from 'app/utils/segment'; import { @@ -17,6 +18,15 @@ import { retryGetPostsAction, } from 'app/actions/views/channel'; +export function startDataCleanup() { + return async (dispatch, getState) => { + dispatch({ + type: ViewTypes.DATA_CLEANUP, + payload: getState(), + }); + }; +} + export function loadConfigAndLicense() { return async (dispatch, getState) => { const {currentUserId} = getState().entities.users; diff --git a/app/app.js b/app/app.js new file mode 100644 index 000000000..1bf721e35 --- /dev/null +++ b/app/app.js @@ -0,0 +1,290 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable global-require*/ +import {AsyncStorage, NativeModules} from 'react-native'; +import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain'; + +import {loadMe} from 'mattermost-redux/actions/users'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; + +import {ViewTypes} from 'app/constants'; +import mattermostManaged from 'app/mattermost_managed'; +import tracker from 'app/utils/time_tracker'; +import {getCurrentLocale} from 'app/selectors/i18n'; + +import {getTranslations as getLocalTranslations} from 'app/i18n'; +import {store, handleManagedConfig} from 'app/mattermost'; +import avoidNativeBridge from 'app/utils/avoid_native_bridge'; + +const {Initialization} = NativeModules; + +const DEVICE_SECURE_CACHE_KEY = 'DEVICE_SECURE_CACHE_KEY'; +const TOOLBAR_BACKGROUND = 'TOOLBAR_BACKGROUND'; +const TOOLBAR_TEXT_COLOR = 'TOOLBAR_TEXT_COLOR'; +const APP_BACKGROUND = 'APP_BACKGROUND'; + +export default class App { + constructor() { + // Usage: app.js + this.shouldRelaunchWhenActive = false; + this.inBackgroundSince = null; + + // Usage: screen/entry.js + this.startAppFromPushNotification = false; + this.isNotificationsConfigured = false; + this.allowOtherServers = true; + this.appStarted = false; + this.emmEnabled = false; + this.translations = null; + this.toolbarBackground = null; + this.toolbarTextColor = null; + this.appBackground = null; + + // Usage utils/push_notifications.js + this.replyNotificationData = null; + this.deviceToken = null; + + // Usage credentials + this.currentUserId = null; + this.token = null; + this.url = null; + + this.getStartupThemes(); + this.getAppCredentials(); + } + + getTranslations = () => { + if (this.translations) { + return this.translations; + } + + const state = store.getState(); + const locale = getCurrentLocale(state); + + this.translations = getLocalTranslations(locale); + return this.translations; + }; + + getManagedConfig = async () => { + try { + const shouldStart = await avoidNativeBridge( + () => { + return Initialization.managedConfigResult; + }, + () => { + return Initialization.managedConfigResult; + }, + () => { + return AsyncStorage.getItem(DEVICE_SECURE_CACHE_KEY); + } + ); + if (shouldStart !== null) { + return shouldStart === 'true'; + } + } catch (error) { + return false; + } + return false; + }; + + getAppCredentials = async () => { + try { + const credentials = await avoidNativeBridge( + () => { + return Initialization.credentialsExist; + }, + () => { + return Initialization.credentials; + }, + () => { + return getGenericPassword(); + } + ); + + if (credentials) { + const usernameParsed = credentials.username.split(','); + const passwordParsed = credentials.password.split(','); + + // username == deviceToken, currentUserId + // password == token, url + if (usernameParsed.length === 2 && passwordParsed.length === 2) { + const [deviceToken, currentUserId] = usernameParsed; + const [token, url] = passwordParsed; + + this.deviceToken = deviceToken; + this.currentUserId = currentUserId; + this.token = token; + this.url = url; + } + } + } catch (error) { + return null; + } + + return null; + }; + + getStartupThemes = async () => { + try { + const [ + toolbarBackground, + toolbarTextColor, + appBackground, + ] = await avoidNativeBridge( + () => { + return Initialization.themesExist; + }, + () => { + return [ + Initialization.toolbarBackground, + Initialization.toolbarTextColor, + Initialization.appBackground, + ]; + }, + () => { + return Promise.all([ + AsyncStorage.getItem(TOOLBAR_BACKGROUND), + AsyncStorage.getItem(TOOLBAR_TEXT_COLOR), + AsyncStorage.getItem(APP_BACKGROUND), + ]); + } + ); + + if (toolbarBackground) { + this.toolbarBackground = toolbarBackground; + this.toolbarTextColor = toolbarTextColor; + this.appBackground = appBackground; + } + } catch (error) { + return null; + } + + return null; + }; + + setManagedConfig = (shouldStart) => { + AsyncStorage.setItem(DEVICE_SECURE_CACHE_KEY, shouldStart.toString()); + }; + + setAppCredentials = (deviceToken, currentUserId, token, url) => { + if (!currentUserId) { + return; + } + const username = `${deviceToken}, ${currentUserId}`; + const password = `${token},${url}`; + setGenericPassword(username, password); + }; + + setStartupThemes = (toolbarBackground, toolbarTextColor, appBackground) => { + AsyncStorage.setItem(TOOLBAR_BACKGROUND, toolbarBackground); + AsyncStorage.setItem(TOOLBAR_TEXT_COLOR, toolbarTextColor); + AsyncStorage.setItem(APP_BACKGROUND, appBackground); + }; + + setStartAppFromPushNotification = (startAppFromPushNotification) => { + this.startAppFromPushNotification = startAppFromPushNotification; + }; + + setIsNotificationsConfigured = (isNotificationsConfigured) => { + this.isNotificationsConfigured = isNotificationsConfigured; + }; + + setAllowOtherServers = (allowOtherServers) => { + this.allowOtherServers = allowOtherServers; + }; + + setAppStarted = (appStarted) => { + this.appStarted = appStarted; + }; + + setEMMEnabled = (emmEnabled) => { + this.emmEnabled = emmEnabled; + }; + + setDeviceToken = (deviceToken) => { + this.deviceToken = deviceToken; + }; + + setReplyNotificationData = (replyNotificationData) => { + this.replyNotificationData = replyNotificationData; + }; + + setInBackgroundSince = (inBackgroundSince) => { + this.inBackgroundSince = inBackgroundSince; + }; + + setShouldRelaunchWhenActive = (shouldRelaunchWhenActive) => { + this.shouldRelaunchWhenActive = shouldRelaunchWhenActive; + }; + + clearNativeCache = () => { + resetGenericPassword(); + AsyncStorage.multiRemove([ + DEVICE_SECURE_CACHE_KEY, + TOOLBAR_BACKGROUND, + TOOLBAR_TEXT_COLOR, + APP_BACKGROUND, + ]); + }; + + verifyManagedConfigCache = async (shouldStartCache) => { + // since we are caching managed device results + // we should verify after using the cache + const shouldStart = await handleManagedConfig(); + if (shouldStartCache && !shouldStart) { + this.setManagedConfig(false); + mattermostManaged.quitApp(); + return; + } + + this.setManagedConfig(true); + }; + + launchApp = async () => { + const shouldStartCache = await this.getManagedConfig(); + if (shouldStartCache) { + this.startApp(); + this.verifyManagedConfigCache(shouldStartCache); + return; + } + + const shouldStart = await handleManagedConfig(); + if (shouldStart) { + this.setManagedConfig(shouldStart); + this.startApp(); + } + }; + + startApp = () => { + if (this.appStarted) { + return; + } + + const {dispatch, getState} = store; + const {entities} = getState(); + + let screen = 'SelectServer'; + if (entities) { + const {credentials} = entities.general; + + if (credentials.token && credentials.url) { + screen = 'Channel'; + tracker.initialLoad = Date.now(); + loadMe()(dispatch, getState); + } + } + + switch (screen) { + case 'SelectServer': + EventEmitter.emit(ViewTypes.LAUNCH_LOGIN, true); + break; + case 'Channel': + EventEmitter.emit(ViewTypes.LAUNCH_CHANNEL, true); + break; + } + + this.setStartAppFromPushNotification(false); + this.setAppStarted(true); + } +} diff --git a/app/components/start/empty_toolbar.js b/app/components/start/empty_toolbar.js new file mode 100644 index 000000000..87b9dc59a --- /dev/null +++ b/app/components/start/empty_toolbar.js @@ -0,0 +1,96 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {Platform, View} from 'react-native'; +import DeviceInfo from 'react-native-device-info'; + +import {ViewTypes} from 'app/constants'; +import {makeStyleSheetFromTheme} from 'app/utils/theme'; + +import Icon from 'react-native-vector-icons/Ionicons'; + +const { + ANDROID_TOP_LANDSCAPE, + ANDROID_TOP_PORTRAIT, + IOS_TOP_LANDSCAPE, + IOS_TOP_PORTRAIT, + STATUS_BAR_HEIGHT, +} = ViewTypes; + +export default class EmptyToolbar extends PureComponent { + static propTypes = { + isLandscape: PropTypes.bool.isRequired, + theme: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + + this.isX = DeviceInfo.getModel() === 'iPhone X'; + } + + render() { + const {isLandscape, theme} = this.props; + + const style = getStyleFromTheme(theme); + const padding = {paddingHorizontal: 0}; + + let height; + switch (Platform.OS) { + case 'android': + height = ANDROID_TOP_PORTRAIT; + if (isLandscape) { + height = ANDROID_TOP_LANDSCAPE; + } + break; + case 'ios': + height = IOS_TOP_PORTRAIT - STATUS_BAR_HEIGHT; + if (isLandscape) { + height = IOS_TOP_LANDSCAPE; + } + + if (this.isX && isLandscape) { + padding.paddingHorizontal = 10; + } + break; + } + + return ( + + + + + + + + ); + } +} + +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return { + header: { + backgroundColor: theme.sidebarHeaderBg, + flexDirection: 'row', + justifyContent: 'flex-start', + width: '100%', + zIndex: 10, + }, + button_container: { + width: 55, + }, + button_wrapper: { + alignItems: 'center', + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + paddingHorizontal: 10, + }, + }; +}); diff --git a/app/components/status_bar/status_bar.js b/app/components/status_bar/status_bar.js index f7f6da0ba..446477929 100644 --- a/app/components/status_bar/status_bar.js +++ b/app/components/status_bar/status_bar.js @@ -12,11 +12,15 @@ import tinyColor from 'tinycolor2'; export default class StatusBar extends PureComponent { static propTypes = { theme: PropTypes.object.isRequired, + headerColor: PropTypes.string, }; render() { const {theme} = this.props; - const headerColor = tinyColor(theme.sidebarHeaderBg); + let headerColor = tinyColor(theme.sidebarHeaderBg); + if (this.props.headerColor) { + headerColor = tinyColor(this.props.headerColor); + } let barStyle = 'light-content'; if (headerColor.isLight() && Platform.OS === 'ios') { barStyle = 'dark-content'; diff --git a/app/constants/view.js b/app/constants/view.js index 5f53b7a36..80bd25d9d 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -64,6 +64,9 @@ const ViewTypes = keyMirror({ ANNOUNCEMENT_BANNER: null, INCREMENT_EMOJI_PICKER_PAGE: null, + + LAUNCH_LOGIN: null, + LAUNCH_CHANNEL: null, }); export default { diff --git a/app/mattermost.js b/app/mattermost.js index 367149bd9..42c80ea55 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -1,660 +1,462 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +/* eslint-disable global-require*/ import 'babel-polyfill'; -import Orientation from 'react-native-orientation'; -import {Provider} from 'react-redux'; -import {Navigation, NativeEventsReceiver} from 'react-native-navigation'; -import {IntlProvider} from 'react-intl'; import { Alert, AppState, InteractionManager, - Keyboard, - NativeModules, Platform, + NativeModules, + Keyboard, } from 'react-native'; +const {StatusBarManager, MattermostShare, Initialization} = NativeModules; + import DeviceInfo from 'react-native-device-info'; -import {setJSExceptionHandler, setNativeExceptionHandler} from 'react-native-exception-handler'; -import StatusBarSizeIOS from 'react-native-status-bar-size'; +import {Navigation, NativeEventsReceiver} from 'react-native-navigation'; +import {Provider} from 'react-redux'; import semver from 'semver'; -import {General} from 'mattermost-redux/constants'; -import {setAppState, setDeviceToken, setServerVersion} from 'mattermost-redux/actions/general'; -import {markChannelAsRead} from 'mattermost-redux/actions/channels'; -import {setSystemEmojis} from 'mattermost-redux/actions/emojis'; -import {logError} from 'mattermost-redux/actions/errors'; -import {loadMe, logout} from 'mattermost-redux/actions/users'; -import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; import {Client4} from 'mattermost-redux/client'; +import {General} from 'mattermost-redux/constants'; +import {setAppState, setServerVersion} from 'mattermost-redux/actions/general'; +import {loadMe, logout} from 'mattermost-redux/actions/users'; +import {handleLoginIdChanged} from 'app/actions/views/login'; +import {handleServerUrlChanged} from 'app/actions/views/select_server'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import initialState from 'app/initial_state'; +import configureStore from 'app/store'; +import {NavigationTypes} from 'app/constants'; +import mattermostBucket from 'app/mattermost_bucket'; +import mattermostManaged from 'app/mattermost_managed'; +import {configurePushNotifications} from 'app/utils/push_notifications'; +import PushNotifications from 'app/push_notifications'; +import {registerScreens} from 'app/screens'; import { calculateDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight, } from 'app/actions/device'; -import { - createPost, - loadConfigAndLicense, - loadFromPushNotification, - purgeOfflineStore, -} from 'app/actions/views/root'; +import {loadConfigAndLicense, startDataCleanup} from 'app/actions/views/root'; import {setChannelDisplayName} from 'app/actions/views/channel'; -import {handleLoginIdChanged} from 'app/actions/views/login'; -import {handleServerUrlChanged} from 'app/actions/views/select_server'; -import {NavigationTypes, ViewTypes} from 'app/constants'; -import {getTranslations} from 'app/i18n'; -import initialState from 'app/initial_state'; -import PushNotifications from 'app/push_notifications'; -import {registerScreens} from 'app/screens'; -import configureStore from 'app/store'; -import mattermostBucket from 'app/mattermost_bucket'; -import mattermostManaged from 'app/mattermost_managed'; import {deleteFileCache} from 'app/utils/file'; -import {init as initAnalytics} from 'app/utils/segment'; -import {captureException, initializeSentry, LOGGER_JAVASCRIPT, LOGGER_NATIVE} from 'app/utils/sentry'; -import tracker from 'app/utils/time_tracker'; -import {stripTrailingSlashes} from 'app/utils/url'; -import {EmojiIndicesByAlias} from 'app/utils/emojis'; - +import avoidNativeBridge from 'app/utils/avoid_native_bridge'; import LocalConfig from 'assets/config'; -const {StatusBarManager} = NativeModules; +import App from './app'; + const AUTHENTICATION_TIMEOUT = 5 * 60 * 1000; -export default class Mattermost { - constructor() { - this.isConfigured = false; - this.allowOtherServers = true; - this.startAppFromPushNotification = false; - this.emmEnabled = false; +export const app = new App(); +export const store = configureStore(initialState); +registerScreens(store, Provider); - Client4.setUserAgent(DeviceInfo.getUserAgent()); - Orientation.unlockAllOrientations(); - initializeSentry(); - this.store = configureStore(initialState); - registerScreens(this.store, Provider); +const lazyLoadExternalModules = () => { + const Orientation = require('react-native-orientation'); + const StatusBarSizeIOS = require('react-native-status-bar-size'); + const initializeErrorHandling = require('app/utils/error_handling').initializeErrorHandling; + return { + Orientation, + StatusBarSizeIOS, + initializeErrorHandling, + }; +}; - this.unsubscribeFromStore = this.store.subscribe(this.listenForHydration); - AppState.addEventListener('change', this.handleAppStateChange); - EventEmitter.on(General.SERVER_VERSION_CHANGED, this.handleServerVersionChanged); - EventEmitter.on(General.CONFIG_CHANGED, this.handleConfigChanged); - EventEmitter.on(NavigationTypes.NAVIGATION_RESET, this.handleLogout); - EventEmitter.on(General.DEFAULT_CHANNEL, this.handleResetChannelDisplayName); - EventEmitter.on(NavigationTypes.RESTART_APP, this.restartApp); - Orientation.addOrientationListener(this.orientationDidChange); - mattermostManaged.addEventListener('managedConfigDidChange', this.handleManagedConfig); +const lazyLoadAnalytics = () => { + const initAnalytics = require('app/utils/segment').init; - if (Platform.OS === 'ios') { - StatusBarSizeIOS.addEventListener('willChange', this.handleStatusBarHeightChange); - } + return { + initAnalytics, + }; +}; - setJSExceptionHandler(this.errorHandler, false); - setNativeExceptionHandler(this.nativeErrorHandler, false); - setSystemEmojis(EmojiIndicesByAlias); +const initializeModules = () => { + const { + Orientation, + StatusBarSizeIOS, + initializeErrorHandling, + } = lazyLoadExternalModules(); + const { + config, + } = store.getState().entities.general; + const orientation = Orientation.getInitialOrientation(); + + Orientation.unlockAllOrientations(); + initializeErrorHandling(); + + EventEmitter.on(NavigationTypes.NAVIGATION_RESET, handleLogout); + EventEmitter.on(NavigationTypes.RESTART_APP, restartApp); + EventEmitter.on(General.SERVER_VERSION_CHANGED, handleServerVersionChanged); + EventEmitter.on(General.CONFIG_CHANGED, handleConfigChanged); + EventEmitter.on(General.DEFAULT_CHANNEL, handleResetChannelDisplayName); + Orientation.addOrientationListener(handleOrientationChange); + mattermostManaged.addEventListener('managedConfigDidChange', handleManagedConfig); + + if (config) { + configureAnalytics(config); } - errorHandler = (e, isFatal) => { - if (!e) { - // This method gets called for propTypes errors in dev mode without an exception - return; - } + if (orientation) { + handleOrientationChange(orientation); + } - console.warn('Handling Javascript error ' + JSON.stringify(e)); // eslint-disable-line no-console - captureException(e, LOGGER_JAVASCRIPT, this.store); + if (Platform.OS === 'ios') { + StatusBarSizeIOS.addEventListener('willChange', handleStatusBarHeightChange); - const intl = this.getIntl(); - closeWebSocket()(this.store.dispatch, this.store.getState); + StatusBarManager.getHeight( + (data) => { + handleStatusBarHeightChange(data.height); + } + ); + } +}; - if (Client4.getUrl()) { - logError(e)(this.store.dispatch); - } +const configureAnalytics = (config) => { + const { + initAnalytics, + } = lazyLoadAnalytics(); + if (config && config.DiagnosticsEnabled === 'true' && config.DiagnosticId && LocalConfig.SegmentApiKey) { + initAnalytics(config); + } else { + global.analytics = null; + } +}; - if (isFatal) { +const resetBadgeAndVersion = () => { + Client4.serverVersion = ''; + Client4.setUserId(''); + PushNotifications.setApplicationIconBadgeNumber(0); + PushNotifications.cancelAllLocalNotifications(); + store.dispatch(setServerVersion('')); +}; + +const handleLogout = () => { + app.setAppStarted(false); + app.clearNativeCache(); + deleteFileCache(); + resetBadgeAndVersion(); + launchSelectServer(); +}; + +const restartApp = async () => { + Navigation.dismissModal({animationType: 'none'}); + + await store.dispatch(loadConfigAndLicense()); + await store.dispatch(loadMe()); + launchChannel(); +}; + +const handleServerVersionChanged = async (serverVersion) => { + const {dispatch, getState} = store; + const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0]; + const translations = app.getTranslations(); + const state = getState(); + + if (serverVersion) { + if (semver.valid(version) && semver.lt(version, LocalConfig.MinServerVersion)) { Alert.alert( - intl.formatMessage({id: 'mobile.error_handler.title', defaultMessage: 'Unexpected error occurred'}), - intl.formatMessage({id: 'mobile.error_handler.description', defaultMessage: '\nClick relaunch to open the app again. After restart, you can report the problem from the settings menu.\n'}), + translations['mobile.server_upgrade.title'], + translations['mobile.server_upgrade.description'], [{ - text: intl.formatMessage({id: 'mobile.error_handler.button', defaultMessage: 'Relaunch'}), - onPress: () => { - // purge the store - this.store.dispatch(purgeOfflineStore()); - }, + text: translations['mobile.server_upgrade.button'], + onPress: handleServerVersionUpgradeNeeded, }], {cancelable: false} ); + } else if (state.entities.users && state.entities.users.currentUserId) { + dispatch(setServerVersion(serverVersion)); + const data = await dispatch(loadConfigAndLicense()); + configureAnalytics(data.config); } - }; + } +}; - nativeErrorHandler = (e) => { - console.warn('Handling native error ' + JSON.stringify(e)); // eslint-disable-line no-console - captureException(e, LOGGER_NATIVE, this.store); - }; +const handleConfigChanged = (config) => { + configureAnalytics(config); +}; - getIntl = () => { - const state = this.store.getState(); - let locale = DeviceInfo.getDeviceLocale().split('-')[0]; - if (state.views.i18n.locale) { - locale = state.views.i18n.locale; - } +const handleServerVersionUpgradeNeeded = async () => { + const {dispatch, getState} = store; - const intlProvider = new IntlProvider({locale, messages: getTranslations(locale)}, {}); - const {intl} = intlProvider.getChildContext(); - return intl; - }; + resetBadgeAndVersion(); - configureAnalytics = (config) => { - if (config && config.DiagnosticsEnabled === 'true' && config.DiagnosticId && LocalConfig.SegmentApiKey) { - initAnalytics(config); - } else { - global.analytics = null; - } - }; - - configurePushNotifications = () => { - PushNotifications.configure({ - onRegister: this.onRegisterDevice, - onNotification: this.onPushNotification, - onReply: this.onPushNotificationReply, - popInitialNotification: true, - requestPermissions: true, + if (getState().entities.general.credentials.token) { + InteractionManager.runAfterInteractions(() => { + dispatch(logout()); }); - this.isConfigured = true; - }; + } +}; - handleAppStateChange = async (appState) => { - const {dispatch, getState} = this.store; - const isActive = appState === 'active'; +const handleStatusBarHeightChange = (nextStatusBarHeight) => { + store.dispatch(setStatusBarHeight(nextStatusBarHeight)); +}; - setAppState(isActive)(dispatch, getState); - - if (isActive && this.shouldRelaunchWhenActive) { - // This handles when the app was started in the background - // cause of an iOS push notification reply - this.launchApp(); - this.shouldRelaunchWhenActive = false; - } else if (!isActive && !this.inBackgroundSince) { - // When the app is sent to the background we set the time when that happens - // and perform a data clean up to improve on performance - this.inBackgroundSince = Date.now(); - dispatch({type: ViewTypes.DATA_CLEANUP, payload: getState()}); - } else if (isActive && this.inBackgroundSince && (Date.now() - this.inBackgroundSince) >= AUTHENTICATION_TIMEOUT && this.emmEnabled) { - // Once the app becomes active after more than 5 minutes in the background and is controlled by an EMM - try { - const config = await mattermostManaged.getConfig(); - const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true'; - if (authNeeded) { - const authenticated = await this.handleAuthentication(config.vendor); - if (!authenticated) { - mattermostManaged.quitApp(); - } - } - } catch (error) { - // do nothing - } - } - - if (isActive) { - this.inBackgroundSince = null; - Keyboard.dismiss(); - } - }; - - handleAuthentication = async (vendor) => { - const isSecured = await mattermostManaged.isDeviceSecure(); - - const intl = this.getIntl(); - if (isSecured) { - try { - mattermostBucket.setPreference('emm', vendor, LocalConfig.AppGroupId); - await mattermostManaged.authenticate({ - reason: intl.formatMessage({ - id: 'mobile.managed.secured_by', - defaultMessage: 'Secured by {vendor}', - }, {vendor}), - fallbackToPasscode: true, - suppressEnterPassword: true, - }); - } catch (err) { - mattermostManaged.quitApp(); - return false; - } - } - - return true; - }; - - handleServerVersionChanged = async (serverVersion) => { - const {dispatch, getState} = this.store; - const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0]; - const intl = this.getIntl(); - const state = getState(); - - if (serverVersion) { - if (semver.valid(version) && semver.lt(version, LocalConfig.MinServerVersion)) { - Alert.alert( - intl.formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'}), - intl.formatMessage({id: 'mobile.server_upgrade.description', defaultMessage: '\nA server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.\n'}), - [{ - text: intl.formatMessage({id: 'mobile.server_upgrade.button', defaultMessage: 'OK'}), - onPress: this.handleServerVersionUpgradeNeeded, - }], - {cancelable: false} - ); - } else if (state.entities.users && state.entities.users.currentUserId) { - setServerVersion(serverVersion)(dispatch, getState); - - // Note that license and config changes are now emitted as websocket events, but - // we might be connected to an older server. Loading the configuration multiple - // times isn't a high overhead at present, so there's no harm in potentially - // repeating the load and handling for now. - const data = await loadConfigAndLicense()(dispatch, getState); - this.handleConfigChanged(data.config); - } - } - }; - - handleConfigChanged = (config) => { - this.configureAnalytics(config); +const handleOrientationChange = (orientation) => { + const {dispatch} = store; + if (DeviceInfo.isTablet()) { + dispatch(setDeviceAsTablet()); } - handleManagedConfig = async (serverConfig) => { - const {dispatch, getState} = this.store; - const state = getState(); + dispatch(setDeviceOrientation(orientation)); + setTimeout(() => { + dispatch(calculateDeviceDimensions()); + }, 100); +}; - let authNeeded = false; - let blurApplicationScreen = false; - let jailbreakProtection = false; - let vendor = null; - let serverUrl = null; - let username = null; +export const handleManagedConfig = async (serverConfig) => { + const {dispatch, getState} = store; + const state = getState(); - if (LocalConfig.AutoSelectServerUrl) { - handleServerUrlChanged(LocalConfig.DefaultServerUrl)(dispatch, getState); - this.allowOtherServers = false; + let authNeeded = false; + let blurApplicationScreen = false; + let jailbreakProtection = false; + let vendor = null; + let serverUrl = null; + let username = null; + + if (LocalConfig.AutoSelectServerUrl) { + dispatch(handleServerUrlChanged(LocalConfig.DefaultServerUrl)); + app.setAllowOtherServers(false); + } + + try { + const config = await avoidNativeBridge( + () => { + return true; + }, + () => { + return Initialization.managedConfig; + }, + () => { + return mattermostManaged.getConfig(); + } + ); + if (config && Object.keys(config).length) { + app.setEMMEnabled(true); + authNeeded = config.inAppPinCode && config.inAppPinCode === 'true'; + blurApplicationScreen = config.blurApplicationScreen && config.blurApplicationScreen === 'true'; + jailbreakProtection = config.jailbreakProtection && config.jailbreakProtection === 'true'; + vendor = config.vendor || 'Mattermost'; + + if (!state.entities.general.credentials.token) { + serverUrl = config.serverUrl; + username = config.username; + + if (config.allowOtherServers && config.allowOtherServers === 'false') { + app.setAllowOtherServers(false); + } + } + + if (jailbreakProtection) { + const isTrusted = mattermostManaged.isTrustedDevice(); + + if (!isTrusted) { + const translations = app.getTranslations(); + Alert.alert( + translations['mobile.managed.blocked_by'].replace('{vendor}', vendor), + translations['mobile.managed.jailbreak'].replace('{vendor}', vendor), + [{ + text: translations['mobile.managed.exit'], + style: 'destructive', + onPress: () => { + mattermostManaged.quitApp(); + }, + }], + {cancelable: false} + ); + return false; + } + } + + if (authNeeded && !serverConfig) { + const authenticated = await handleAuthentication(vendor); + if (!authenticated) { + return false; + } + } + + if (blurApplicationScreen) { + mattermostManaged.blurAppScreen(true); + } + + if (serverUrl) { + dispatch(handleServerUrlChanged(serverUrl)); + } + + if (username) { + dispatch(handleLoginIdChanged(username)); + } } + } catch (error) { + return true; + } + return true; +}; + +const handleAuthentication = async (vendor) => { + const isSecured = await mattermostManaged.isDeviceSecure(); + + const translations = app.getTranslations(); + if (isSecured) { + try { + mattermostBucket.setPreference('emm', vendor, LocalConfig.AppGroupId); + await mattermostManaged.authenticate({ + reason: translations['mobile.managed.secured_by'].replace('{vendor}', vendor), + fallbackToPasscode: true, + suppressEnterPassword: true, + }); + } catch (err) { + mattermostManaged.quitApp(); + return false; + } + } + + return true; +}; + +const handleResetChannelDisplayName = (displayName) => { + store.dispatch(setChannelDisplayName(displayName)); +}; + +const launchSelectServer = () => { + Navigation.startSingleScreenApp({ + screen: { + screen: 'SelectServer', + navigatorStyle: { + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false, + screenBackgroundColor: 'transparent', + }, + }, + passProps: { + allowOtherServers: app.allowOtherServers, + }, + appStyle: { + orientation: 'auto', + }, + animationType: 'fade', + }); +}; + +const launchChannel = () => { + Navigation.startSingleScreenApp({ + screen: { + screen: 'Channel', + navigatorStyle: { + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false, + screenBackgroundColor: 'transparent', + }, + }, + appStyle: { + orientation: 'auto', + }, + animationType: 'fade', + }); +}; + +const handleAppStateChange = (appState) => { + const isActive = appState === 'active'; + + store.dispatch(setAppState(isActive)); + + if (isActive) { + handleAppActive(); + return; + } + + handleAppInActive(); +}; + +const handleAppActive = async () => { + const authExpired = (Date.now() - app.inBackgroundSince) >= AUTHENTICATION_TIMEOUT; + + // This handles when the app was started in the background + // cause of an iOS push notification reply + if (Platform.OS === 'ios' && app.shouldRelaunchWhenActive) { + app.launchApp(); + app.setShouldRelaunchWhenActive(false); + } + + // Once the app becomes active after more than 5 minutes in the background and is controlled by an EMM Provider + if (app.emmEnabled && app.inBackgroundSince && authExpired) { try { const config = await mattermostManaged.getConfig(); - if (config && Object.keys(config).length) { - this.emmEnabled = true; - authNeeded = config.inAppPinCode && config.inAppPinCode === 'true'; - blurApplicationScreen = config.blurApplicationScreen && config.blurApplicationScreen === 'true'; - jailbreakProtection = config.jailbreakProtection && config.jailbreakProtection === 'true'; - vendor = config.vendor || 'Mattermost'; - - if (!state.entities.general.credentials.token) { - serverUrl = config.serverUrl; - username = config.username; - - if (config.allowOtherServers && config.allowOtherServers === 'false') { - this.allowOtherServers = false; - } - } - - if (jailbreakProtection) { - const isTrusted = mattermostManaged.isTrustedDevice(); - - if (!isTrusted) { - const intl = this.getIntl(); - Alert.alert( - intl.formatMessage({ - id: 'mobile.managed.blocked_by', - defaultMessage: 'Blocked by {vendor}', - }, {vendor}), - intl.formatMessage({ - id: 'mobile.managed.jailbreak', - defaultMessage: 'Jailbroken devices are not trusted by {vendor}, please exit the app.', - }, {vendor}), - [{ - text: intl.formatMessage({id: 'mobile.managed.exit', defaultMessage: 'Exit'}), - style: 'destructive', - onPress: () => { - mattermostManaged.quitApp(); - }, - }], - {cancelable: false} - ); - return false; - } - } - - if (authNeeded && !serverConfig) { - if (Platform.OS === 'android') { - //Start a fake app as we need at least one activity for android - await this.startFakeApp(); - } - const authenticated = await this.handleAuthentication(vendor); - if (!authenticated) { - return false; - } - } - - if (blurApplicationScreen) { - mattermostManaged.blurAppScreen(true); - } - - if (serverUrl) { - handleServerUrlChanged(serverUrl)(dispatch, getState); - } - - if (username) { - handleLoginIdChanged(username)(dispatch, getState); + const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true'; + if (authNeeded) { + const authenticated = await handleAuthentication(config.vendor); + if (!authenticated) { + mattermostManaged.quitApp(); } } } catch (error) { - return true; + // do nothing } + } - return true; - }; + app.setInBackgroundSince(null); + Keyboard.dismiss(); +}; - handleLogout = () => { - this.appStarted = false; - deleteFileCache(); - this.resetBadgeAndVersion(); - this.startApp('fade'); - }; +const handleAppInActive = () => { + const {dispatch, getState} = store; + const theme = getTheme(getState()); - handleResetChannelDisplayName = (displayName) => { - this.store.dispatch(setChannelDisplayName(displayName)); - }; + // When the app is sent to the background we set the time when that happens + // and perform a data clean up to improve on performance + app.setInBackgroundSince(Date.now()); + app.setStartupThemes( + theme.sidebarHeaderBg, + theme.sidebarHeaderTextColor, + theme.centerChannelBg, + ); + dispatch(startDataCleanup()); +}; - handleStatusBarHeightChange = (nextStatusBarHeight) => { - this.store.dispatch(setStatusBarHeight(nextStatusBarHeight)); - }; +AppState.addEventListener('change', handleAppStateChange); - handleServerVersionUpgradeNeeded = async () => { - const {dispatch, getState} = this.store; - - this.resetBadgeAndVersion(); - - if (getState().entities.general.credentials.token) { - InteractionManager.runAfterInteractions(() => { - logout()(dispatch, getState); - }); - } - }; - - // We need to wait for hydration to occur before load the router. - listenForHydration = () => { - const {dispatch, getState} = this.store; - const state = getState(); - if (!this.isConfigured) { - this.configurePushNotifications(); - } - - if (state.views.root.hydrationComplete) { - const orientation = Orientation.getInitialOrientation(); - const {credentials, config} = state.entities.general; - const {currentUserId} = state.entities.users; - const isNotActive = AppState.currentState !== 'active'; - const notification = PushNotifications.getNotification(); - - this.unsubscribeFromStore(); - - if (this.deviceToken) { - // If the deviceToken is set we need to dispatch it to the redux store - setDeviceToken(this.deviceToken)(dispatch, getState); - } - - if (orientation) { - this.orientationDidChange(orientation); - } - - if (config) { - this.configureAnalytics(config); - } - - if (credentials.token && credentials.url) { - Client4.setToken(credentials.token); - Client4.setUrl(stripTrailingSlashes(credentials.url)); - } - - if (currentUserId) { - Client4.setUserId(currentUserId); - } - - if (Platform.OS === 'ios') { - StatusBarManager.getHeight( - (data) => { - this.handleStatusBarHeightChange(data.height); - } - ); - } - - if (notification || this.replyNotificationData) { - // If we have a notification means that the app was started cause of a reply - // and the app was not sitting in the background nor opened - const notificationData = notification || this.replyNotificationData; - const {data, text, badge, completed} = notificationData; - this.onPushNotificationReply(data, text, badge, completed); - PushNotifications.resetNotification(); - } - - if (Platform.OS === 'android') { - // In case of Android we need to handle the bridge being initialized by HeadlessJS - Promise.resolve(Navigation.isAppLaunched()).then((appLaunched) => { - if (this.startAppFromPushNotification) { - return; - } - - if (appLaunched) { - this.launchApp(); // App is launched -> show UI - } else { - new NativeEventsReceiver().appLaunched(this.launchApp); // App hasn't been launched yet -> show the UI only when needed. - } - }); - } else if (isNotActive) { - // for IOS replying from push notification starts the app in the background - this.shouldRelaunchWhenActive = true; - this.startFakeApp(); - } else { - this.launchApp(); - } - } - }; - - onRegisterDevice = (data) => { - this.isConfigured = true; - const {dispatch, getState} = this.store; - let prefix; - if (Platform.OS === 'ios') { - prefix = General.PUSH_NOTIFY_APPLE_REACT_NATIVE; - if (DeviceInfo.getBundleId().includes('rnbeta')) { - prefix = `${prefix}beta`; - } - } else { - prefix = General.PUSH_NOTIFY_ANDROID_REACT_NATIVE; - } - - const state = getState(); - const token = `${prefix}:${data.token}`; - if (state.views.root.hydrationComplete) { - setDeviceToken(token)(dispatch, getState); - } else { - this.deviceToken = token; - } - }; - - onPushNotification = (deviceNotification) => { - const {dispatch, getState} = this.store; - const state = getState(); - const {token, url} = state.entities.general.credentials; - - // mark the app as started as soon as possible - if (!this.appStarted && Platform.OS !== 'ios') { - this.startAppFromPushNotification = true; - } - - const {data, foreground, message, userInfo, userInteraction} = deviceNotification; - const notification = { - data, - message, - }; - - if (userInfo) { - notification.localNotification = userInfo.localNotification; - } - - if (data.type === 'clear') { - markChannelAsRead(data.channel_id, null, false)(dispatch, getState); - } else if (foreground) { - EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification); - } else if (userInteraction && !notification.localNotification) { - EventEmitter.emit('close_channel_drawer'); - if (this.startAppFromPushNotification) { - Client4.setToken(token); - Client4.setUrl(stripTrailingSlashes(url)); - } - - InteractionManager.runAfterInteractions(async () => { - await loadFromPushNotification(notification)(dispatch, getState); - - if (this.startAppFromPushNotification) { - this.launchApp(); - } else { - EventEmitter.emit(ViewTypes.NOTIFICATION_TAPPED); - } - }); - } - }; - - onPushNotificationReply = (data, text, badge, completed) => { - const {dispatch, getState} = this.store; - const state = getState(); - const {currentUserId} = state.entities.users; - - if (currentUserId) { - // one thing to note is that for android it will reply to the last post in the stack - const rootId = data.root_id || data.post_id; - const post = { - user_id: currentUserId, - channel_id: data.channel_id, - root_id: rootId, - parent_id: rootId, - message: text, - }; - - if (!Client4.getUrl()) { - // Make sure the Client has the server url set - Client4.setUrl(state.entities.general.credentials.url); - } - - if (!Client4.getToken()) { - // Make sure the Client has the server token set - Client4.setToken(state.entities.general.credentials.token); - } - - createPost(post)(dispatch, getState).then(() => { - markChannelAsRead(data.channel_id)(dispatch, getState); - - if (badge >= 0) { - PushNotifications.setApplicationIconBadgeNumber(badge); - } - - this.replyNotificationData = null; - }).then(completed); - } else { - this.replyNotificationData = { - data, - text, - badge, - completed, - }; - } - }; - - orientationDidChange = (orientation) => { - const {dispatch} = this.store; - dispatch(setDeviceOrientation(orientation)); - if (DeviceInfo.isTablet()) { - dispatch(setDeviceAsTablet()); - } - setTimeout(() => { - dispatch(calculateDeviceDimensions()); - }, 100); - }; - - resetBadgeAndVersion = () => { - const {dispatch, getState} = this.store; - Client4.serverVersion = ''; - Client4.setUserId(''); - PushNotifications.setApplicationIconBadgeNumber(0); - PushNotifications.cancelAllLocalNotifications(); - setServerVersion('')(dispatch, getState); - }; - - restartApp = async () => { - Navigation.dismissModal({animationType: 'none'}); - - const {dispatch, getState} = this.store; - await loadConfigAndLicense()(dispatch, getState); - await loadMe()(dispatch, getState); - this.appStarted = false; - this.startApp('fade'); - }; - - launchApp = () => { - this.handleManagedConfig().then((shouldStart) => { - if (shouldStart) { - this.startApp('fade'); - } - }); - }; - - startFakeApp = async () => { - return Navigation.startSingleScreenApp({ - screen: { - screen: 'Root', - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - }, +const launchEntry = () => { + Navigation.startSingleScreenApp({ + screen: { + screen: 'Entry', + navigatorStyle: { + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false, }, - }); - }; + }, + passProps: { + initializeModules, + }, + appStyle: { + orientation: 'auto', + }, + animationType: 'fade', + }); +}; - startApp = (animationType = 'none') => { - if (!this.appStarted) { - const {dispatch, getState} = this.store; - const {entities} = getState(); - let screen = 'SelectServer'; +configurePushNotifications(); +const startedSharedExtension = Platform.OS === 'android' && MattermostShare.isOpened; +const fromPushNotification = Platform.OS === 'android' && Initialization.replyFromPushNotification; - if (entities) { - const {credentials} = entities.general; +if (startedSharedExtension || fromPushNotification) { + // Hold on launching Entry screen + app.setAppStarted(true); - if (credentials.token && credentials.url) { - screen = 'Channel'; - tracker.initialLoad = Date.now(); - loadMe()(dispatch, getState); - } - } - - Navigation.startSingleScreenApp({ - screen: { - screen, - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - }, - }, - passProps: { - allowOtherServers: this.allowOtherServers, - }, - appStyle: { - orientation: 'auto', - }, - animationType, - }); - - this.appStarted = true; - this.startAppFromPushNotification = false; - } - }; + // Listen for when the user opens the app + new NativeEventsReceiver().appLaunched(() => { + app.setAppStarted(false); + launchEntry(); + }); +} + +if (!app.appStarted) { + launchEntry(); } diff --git a/app/push_notifications/push_notifications.android.js b/app/push_notifications/push_notifications.android.js index 0cc73a8b9..2c7e004a2 100644 --- a/app/push_notifications/push_notifications.android.js +++ b/app/push_notifications/push_notifications.android.js @@ -35,7 +35,7 @@ class PushNotification { const notification = new Notification(deviceNotification); const data = notification.getData(); - if (this.onReply && AppState.currentState === 'background') { + if (this.onReply) { this.onReply(data, data.text, parseInt(data.badge, 10) - parseInt(data.msg_count, 10)); } else { this.deviceNotification = { diff --git a/app/screens/entry/entry.js b/app/screens/entry/entry.js new file mode 100644 index 000000000..4a8af53f9 --- /dev/null +++ b/app/screens/entry/entry.js @@ -0,0 +1,274 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable global-require*/ +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + View, + AppState, + Platform, +} from 'react-native'; + +import DeviceInfo from 'react-native-device-info'; + +import {Client4} from 'mattermost-redux/client'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; + +import { + app, + store, +} from 'app/mattermost'; +import {ViewTypes} from 'app/constants'; +import PushNotifications from 'app/push_notifications'; +import {stripTrailingSlashes} from 'app/utils/url'; + +import EmptyToolbar from 'app/components/start/empty_toolbar'; +import Loading from 'app/components/loading'; +import SafeAreaView from 'app/components/safe_area_view'; +import StatusBar from 'app/components/status_bar'; + +const lazyLoadSelectServer = () => { + return require('app/screens/select_server').default; +}; + +const lazyLoadChannel = () => { + return require('app/screens/channel').default; +}; + +const lazyLoadPushNotifications = () => { + return require('app/utils/push_notifications').configurePushNotifications; +}; + +const lazyLoadReplyPushNotifications = () => { + return require('app/utils/push_notifications').onPushNotificationReply; +}; + +/** + * Entry Component: + * With very little overhead navigate to + * - Login or + * - Channel Component + * + * The idea is to render something to the user as soon as possible + */ +export default class Entry extends PureComponent { + static propTypes = { + config: PropTypes.object, + theme: PropTypes.object, + navigator: PropTypes.object, + isLandscape: PropTypes.bool, + hydrationComplete: PropTypes.bool, + initializeModules: PropTypes.func.isRequired, + actions: PropTypes.shape({ + setDeviceToken: PropTypes.func.isRequired, + }).isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + launchLogin: false, + launchChannel: false, + }; + } + + componentDidMount() { + Client4.setUserAgent(DeviceInfo.getUserAgent()); + this.unsubscribeFromStore = store.subscribe(this.listenForHydration); + + EventEmitter.on(ViewTypes.LAUNCH_LOGIN, this.handleLaunchLogin); + EventEmitter.on(ViewTypes.LAUNCH_CHANNEL, this.handleLaunchChannel); + } + + componentWillUnmount() { + EventEmitter.off(ViewTypes.LAUNCH_LOGIN, this.handleLaunchLogin); + EventEmitter.off(ViewTypes.LAUNCH_CHANNEL, this.handleLaunchChannel); + } + + handleLaunchLogin = (initializeModules) => { + this.setState({launchLogin: true}); + + if (initializeModules) { + this.props.initializeModules(); + } + }; + + handleLaunchChannel = (initializeModules) => { + this.setState({launchChannel: true}); + + if (initializeModules) { + this.props.initializeModules(); + } + }; + + listenForHydration = () => { + const {getState} = store; + const state = getState(); + + if (!app.isNotificationsConfigured) { + this.configurePushNotifications(); + } + + if (state.views.root.hydrationComplete) { + this.unsubscribeFromStore(); + + this.setAppCredentials(); + this.setStartupThemes(); + this.setReplyNotifications(); + + if (Platform.OS === 'android') { + this.launchForAndroid(); + return; + } + + this.launchForiOS(); + } + }; + + configurePushNotifications = () => { + const configureNotifications = lazyLoadPushNotifications(); + configureNotifications(); + }; + + setAppCredentials = () => { + const { + actions: { + setDeviceToken, + }, + } = this.props; + const {getState} = store; + const state = getState(); + + const {credentials} = state.entities.general; + const {currentUserId} = state.entities.users; + + if (app.deviceToken) { + setDeviceToken(app.deviceToken); + } + + if (credentials.token && credentials.url) { + Client4.setToken(credentials.token); + Client4.setUrl(stripTrailingSlashes(credentials.url)); + } + + if (currentUserId) { + Client4.setUserId(currentUserId); + } + + app.setAppCredentials(app.deviceToken, currentUserId, credentials.token, credentials.url); + }; + + setStartupThemes = () => { + const {theme} = this.props; + if (app.toolbarBackground === theme.sidebarHeaderBg) { + return; + } + + app.setStartupThemes( + theme.sidebarHeaderBg, + theme.sidebarHeaderTextColor, + theme.centerChannelBg + ); + }; + + setReplyNotifications = () => { + const notification = PushNotifications.getNotification(); + + // If notification exists, it means that the app was started through a reply + // and the app was not sitting in the background nor opened + if (notification || app.replyNotificationData) { + const onPushNotificationReply = lazyLoadReplyPushNotifications(); + const notificationData = notification || app.replyNotificationData; + const {data, text, badge, completed} = notificationData; + + onPushNotificationReply(data, text, badge, completed); + PushNotifications.resetNotification(); + } + }; + + launchForAndroid = () => { + if (app.startAppFromPushNotification) { + return; + } + + app.launchApp(); + }; + + launchForiOS = () => { + const appNotActive = AppState.currentState !== 'active'; + + if (appNotActive) { + // for iOS replying from push notification starts the app in the background + app.setShouldRelaunchWhenActive(true); + } else { + app.launchApp(); + } + }; + + renderLogin = () => { + const SelectServer = lazyLoadSelectServer(); + return ( + + ); + }; + + renderChannel = () => { + const ChannelScreen = lazyLoadChannel(); + + return ( + + ); + }; + + render() { + const { + navigator, + isLandscape, + } = this.props; + + if (this.state.launchLogin) { + return this.renderLogin(); + } + + if (this.state.launchChannel) { + return this.renderChannel(); + } + + let toolbar = null; + const backgroundColor = app.appBackground ? app.appBackground : '#ffff'; + if (app.token && app.toolbarBackground) { + const toolbarTheme = { + sidebarHeaderBg: app.toolbarBackground, + sidebarHeaderTextColor: app.toolbarTextColor, + }; + + toolbar = ( + + + + + ); + } + + return ( + + {toolbar} + + + ); + } +} diff --git a/app/screens/entry/index.js b/app/screens/entry/index.js new file mode 100644 index 000000000..23bc2c2bd --- /dev/null +++ b/app/screens/entry/index.js @@ -0,0 +1,36 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable global-require*/ +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {setDeviceToken} from 'mattermost-redux/actions/general'; +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; + +import {isLandscape} from 'app/selectors/device'; + +const lazyLoadEntry = () => { + return require('./entry').default; +}; + +function mapStateToProps(state) { + const {config} = state.entities.general; + + return { + config, + theme: getTheme(state), + isLandscape: isLandscape(state), + hydrationComplete: state.views.root.hydrationComplete, + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + setDeviceToken, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(lazyLoadEntry()); diff --git a/app/screens/index.js b/app/screens/index.js index d04315c30..0e9c3b231 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -4,52 +4,6 @@ import React from 'react'; import {Navigation} from 'react-native-navigation'; -import About from 'app/screens/about'; - -import AddReaction from 'app/screens/add_reaction'; -import AdvancedSettings from 'app/screens/settings/advanced_settings'; -import Channel from 'app/screens/channel'; -import ChannelAddMembers from 'app/screens/channel_add_members'; -import ChannelInfo from 'app/screens/channel_info'; -import ChannelMembers from 'app/screens/channel_members'; -import ChannelPeek from 'app/screens/channel_peek'; -import ClientUpgrade from 'app/screens/client_upgrade'; -import ClockDisplay from 'app/screens/clock_display'; -import Code from 'app/screens/code'; -import CreateChannel from 'app/screens/create_channel'; -import DisplaySettings from 'app/screens/settings/display_settings'; -import EditChannel from 'app/screens/edit_channel'; -import EditPost from 'app/screens/edit_post'; -import EditProfile from 'app/screens/edit_profile'; -import FlaggedPosts from 'app/screens/flagged_posts'; -import ImagePreview from 'app/screens/image_preview'; -import TextPreview from 'app/screens/text_preview'; -import Login from 'app/screens/login'; -import LoginOptions from 'app/screens/login_options'; -import LongPost from 'app/screens/long_post'; -import Mfa from 'app/screens/mfa'; -import MoreChannels from 'app/screens/more_channels'; -import MoreDirectMessages from 'app/screens/more_dms'; -import Notification from 'app/screens/notification'; -import NotificationSettings from 'app/screens/settings/notification_settings'; -import NotificationSettingsEmail from 'app/screens/settings/notification_settings_email'; -import NotificationSettingsMentions from 'app/screens/settings/notification_settings_mentions'; -import NotificationSettingsMentionsKeywords from 'app/screens/settings/notification_settings_mentions_keywords'; -import NotificationSettingsMobile from 'app/screens/settings/notification_settings_mobile'; -import OptionsModal from 'app/screens/options_modal'; -import Permalink from 'app/screens/permalink'; -import RecentMentions from 'app/screens/recent_mentions'; -import Root from 'app/screens/root'; -import SSO from 'app/screens/sso'; -import Search from 'app/screens/search'; -import SelectServer from 'app/screens/select_server'; -import SelectTeam from 'app/screens/select_team'; -import Settings from 'app/screens/settings/general'; -import Table from 'app/screens/table'; -import TableImage from 'app/screens/table_image'; -import Thread from 'app/screens/thread'; -import UserProfile from 'app/screens/user_profile'; - import IntlWrapper from 'app/components/root'; function wrapWithContextProvider(Comp, excludeEvents = true) { @@ -67,48 +21,49 @@ function wrapWithContextProvider(Comp, excludeEvents = true) { } export function registerScreens(store, Provider) { - Navigation.registerComponent('About', () => wrapWithContextProvider(About), store, Provider); - Navigation.registerComponent('AddReaction', () => wrapWithContextProvider(AddReaction), store, Provider); - Navigation.registerComponent('AdvancedSettings', () => wrapWithContextProvider(AdvancedSettings), store, Provider); - Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel, false), store, Provider); - Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider); - Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(ChannelInfo), store, Provider); - Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(ChannelMembers), store, Provider); - Navigation.registerComponent('ChannelPeek', () => wrapWithContextProvider(ChannelPeek), store, Provider); - Navigation.registerComponent('ClientUpgrade', () => wrapWithContextProvider(ClientUpgrade), store, Provider); - Navigation.registerComponent('ClockDisplay', () => wrapWithContextProvider(ClockDisplay), store, Provider); - Navigation.registerComponent('Code', () => wrapWithContextProvider(Code), store, Provider); - Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(CreateChannel), store, Provider); - Navigation.registerComponent('DisplaySettings', () => wrapWithContextProvider(DisplaySettings), store, Provider); - Navigation.registerComponent('EditChannel', () => wrapWithContextProvider(EditChannel), store, Provider); - Navigation.registerComponent('EditPost', () => wrapWithContextProvider(EditPost), store, Provider); - Navigation.registerComponent('EditProfile', () => wrapWithContextProvider(EditProfile), store, Provider); - Navigation.registerComponent('FlaggedPosts', () => wrapWithContextProvider(FlaggedPosts), store, Provider); - Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(ImagePreview), store, Provider); - Navigation.registerComponent('TextPreview', () => wrapWithContextProvider(TextPreview), store, Provider); - Navigation.registerComponent('Login', () => wrapWithContextProvider(Login), store, Provider); - Navigation.registerComponent('LoginOptions', () => wrapWithContextProvider(LoginOptions), store, Provider); - Navigation.registerComponent('LongPost', () => wrapWithContextProvider(LongPost), store, Provider); - Navigation.registerComponent('MFA', () => wrapWithContextProvider(Mfa), store, Provider); - Navigation.registerComponent('MoreChannels', () => wrapWithContextProvider(MoreChannels), store, Provider); - Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(MoreDirectMessages), store, Provider); - Navigation.registerComponent('Notification', () => wrapWithContextProvider(Notification), store, Provider); - Navigation.registerComponent('NotificationSettings', () => wrapWithContextProvider(NotificationSettings), store, Provider); - Navigation.registerComponent('NotificationSettingsEmail', () => wrapWithContextProvider(NotificationSettingsEmail), store, Provider); - Navigation.registerComponent('NotificationSettingsMentions', () => wrapWithContextProvider(NotificationSettingsMentions), store, Provider); - Navigation.registerComponent('NotificationSettingsMentionsKeywords', () => wrapWithContextProvider(NotificationSettingsMentionsKeywords), store, Provider); - Navigation.registerComponent('NotificationSettingsMobile', () => wrapWithContextProvider(NotificationSettingsMobile), store, Provider); - Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(OptionsModal), store, Provider); - Navigation.registerComponent('Permalink', () => wrapWithContextProvider(Permalink), store, Provider); - Navigation.registerComponent('RecentMentions', () => wrapWithContextProvider(RecentMentions), store, Provider); - Navigation.registerComponent('Root', () => Root, store, Provider); - Navigation.registerComponent('Search', () => wrapWithContextProvider(Search), store, Provider); - Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider); - Navigation.registerComponent('SelectTeam', () => wrapWithContextProvider(SelectTeam), store, Provider); - Navigation.registerComponent('Settings', () => wrapWithContextProvider(Settings), store, Provider); - Navigation.registerComponent('SSO', () => wrapWithContextProvider(SSO), store, Provider); - Navigation.registerComponent('Table', () => wrapWithContextProvider(Table), store, Provider); - Navigation.registerComponent('TableImage', () => wrapWithContextProvider(TableImage), store, Provider); - Navigation.registerComponent('Thread', () => wrapWithContextProvider(Thread), store, Provider); - Navigation.registerComponent('UserProfile', () => wrapWithContextProvider(UserProfile), store, Provider); + Navigation.registerComponent('About', () => wrapWithContextProvider(require('app/screens/about').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('AddReaction', () => wrapWithContextProvider(require('app/screens/add_reaction').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('AdvancedSettings', () => wrapWithContextProvider(require('app/screens/settings/advanced_settings').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Channel', () => wrapWithContextProvider(require('app/screens/channel').default, false), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(require('app/screens/channel_add_members').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(require('app/screens/channel_info').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(require('app/screens/channel_members').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('ChannelPeek', () => wrapWithContextProvider(require('app/screens/channel_peek').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('ClientUpgrade', () => wrapWithContextProvider(require('app/screens/client_upgrade').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('ClockDisplay', () => wrapWithContextProvider(require('app/screens/clock_display').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Code', () => wrapWithContextProvider(require('app/screens/code').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(require('app/screens/create_channel').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('DisplaySettings', () => wrapWithContextProvider(require('app/screens/settings/display_settings').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('EditChannel', () => wrapWithContextProvider(require('app/screens/edit_channel').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('EditPost', () => wrapWithContextProvider(require('app/screens/edit_post').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('EditProfile', () => wrapWithContextProvider(require('app/screens/edit_profile').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Entry', () => wrapWithContextProvider(require('app/screens/entry').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('FlaggedPosts', () => wrapWithContextProvider(require('app/screens/flagged_posts').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(require('app/screens/image_preview').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('TextPreview', () => wrapWithContextProvider(require('app/screens/text_preview').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Login', () => wrapWithContextProvider(require('app/screens/login').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('LoginOptions', () => wrapWithContextProvider(require('app/screens/login_options').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('LongPost', () => wrapWithContextProvider(require('app/screens/long_post').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('MFA', () => wrapWithContextProvider(require('app/screens/mfa').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('MoreChannels', () => wrapWithContextProvider(require('app/screens/more_channels').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(require('app/screens/more_dms').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Notification', () => wrapWithContextProvider(require('app/screens/notification').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('NotificationSettings', () => wrapWithContextProvider(require('app/screens/settings/notification_settings').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('NotificationSettingsEmail', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_email').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('NotificationSettingsMentions', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_mentions').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('NotificationSettingsMentionsKeywords', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_mentions_keywords').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('NotificationSettingsMobile', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_mobile').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(require('app/screens/options_modal').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Permalink', () => wrapWithContextProvider(require('app/screens/permalink').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('RecentMentions', () => wrapWithContextProvider(require('app/screens/recent_mentions').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Root', () => require('app/screens/root').default, store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Search', () => wrapWithContextProvider(require('app/screens/search').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(require('app/screens/select_server').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('SelectTeam', () => wrapWithContextProvider(require('app/screens/select_team').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Settings', () => wrapWithContextProvider(require('app/screens/settings/general').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('SSO', () => wrapWithContextProvider(require('app/screens/sso').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Table', () => wrapWithContextProvider(require('app/screens/table').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('TableImage', () => wrapWithContextProvider(require('app/screens/table_image').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('Thread', () => wrapWithContextProvider(require('app/screens/thread').default), store, Provider); //eslint-disable-line global-require + Navigation.registerComponent('UserProfile', () => wrapWithContextProvider(require('app/screens/user_profile').default), store, Provider); //eslint-disable-line global-require } diff --git a/app/utils/avoid_native_bridge.js b/app/utils/avoid_native_bridge.js new file mode 100644 index 000000000..77b2090ac --- /dev/null +++ b/app/utils/avoid_native_bridge.js @@ -0,0 +1,19 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {Platform} from 'react-native'; + +/** + * avoidNativeBridge: a helper function that facilitates returning + * a constant variable packaged from InitializationModule + * or from a NativeModule which will make a call to the native bridge + * + * Currently only required for Android + */ +export default function avoidNativeBridge(runOptimized, optimized, fallback) { + if (Platform.OS === 'android' && runOptimized()) { + return optimized(); + } + + return fallback(); +} diff --git a/app/utils/error_handling.js b/app/utils/error_handling.js new file mode 100644 index 000000000..620b533df --- /dev/null +++ b/app/utils/error_handling.js @@ -0,0 +1,62 @@ +import { + Alert, +} from 'react-native'; + +import { + setJSExceptionHandler, + setNativeExceptionHandler, +} from 'react-native-exception-handler'; + +import {Client4} from 'mattermost-redux/client'; +import {logError} from 'mattermost-redux/actions/errors'; +import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; + +import {purgeOfflineStore} from 'app/actions/views/root'; +import { + captureException, + initializeSentry, + LOGGER_JAVASCRIPT, + LOGGER_NATIVE, +} from 'app/utils/sentry'; + +import {app, store} from 'app/mattermost'; + +const errorHandler = (e, isFatal) => { + console.warn('Handling Javascript error ' + JSON.stringify(e)); // eslint-disable-line no-console + const {dispatch, getState} = store; + + captureException(e, LOGGER_JAVASCRIPT, store); + + const translations = app.getTranslations(); + closeWebSocket()(dispatch, getState); + + if (Client4.getUrl()) { + logError(e)(dispatch, getState); + } + + if (isFatal) { + Alert.alert( + translations['mobile.error_handler.title'], + translations['mobile.error_handler.description'], + [{ + text: translations['mobile.error_handler.button'], + onPress: () => { + // purge the store + purgeOfflineStore()(dispatch, getState); + }, + }], + {cancelable: false} + ); + } +}; + +const nativeErrorHandler = (e) => { + console.warn('Handling native error ' + JSON.stringify(e)); // eslint-disable-line no-console + captureException(e, LOGGER_NATIVE, store); +}; + +export function initializeErrorHandling() { + initializeSentry(); + setJSExceptionHandler(errorHandler, false); + setNativeExceptionHandler(nativeErrorHandler, false); +} diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js new file mode 100644 index 000000000..61f4514a6 --- /dev/null +++ b/app/utils/push_notifications.js @@ -0,0 +1,157 @@ +import { + Platform, + InteractionManager, +} from 'react-native'; + +import PushNotifications from 'app/push_notifications'; +import DeviceInfo from 'react-native-device-info'; +import {Client4} from 'mattermost-redux/client'; + +import {markChannelAsRead} from 'mattermost-redux/actions/channels'; +import {setDeviceToken} from 'mattermost-redux/actions/general'; +import {General} from 'mattermost-redux/constants'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; + +import {ViewTypes} from 'app/constants'; +import { + createPost, + loadFromPushNotification, +} from 'app/actions/views/root'; + +import {stripTrailingSlashes} from 'app/utils/url'; + +import { + app, + store, +} from 'app/mattermost'; + +const onRegisterDevice = (data) => { + app.setIsNotificationsConfigured(true); + const state = store.getState(); + + let prefix; + if (Platform.OS === 'ios') { + prefix = General.PUSH_NOTIFY_APPLE_REACT_NATIVE; + if (DeviceInfo.getBundleId().includes('rnbeta')) { + prefix = `${prefix}beta`; + } + } else { + prefix = General.PUSH_NOTIFY_ANDROID_REACT_NATIVE; + } + + const token = `${prefix}:${data.token}`; + if (state.views.root.hydrationComplete) { + app.setDeviceToken(token); + setDeviceToken(token)(store.dispatch, store.getState); + } else { + app.setDeviceToken(token); + } +}; + +const onPushNotification = (deviceNotification) => { + const {dispatch, getState} = store; + const state = getState(); + const {token, url} = state.entities.general.credentials; + + // mark the app as started as soon as possible + if (Platform.OS === 'android' && !app.appStarted) { + app.setStartAppFromPushNotification(true); + } + + const {data, foreground, message, userInfo, userInteraction} = deviceNotification; + const notification = { + data, + message, + }; + + if (userInfo) { + notification.localNotification = userInfo.localNotification; + } + + if (data.type === 'clear') { + markChannelAsRead(data.channel_id, null, false)(dispatch, getState); + } else if (foreground) { + EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification); + } else if (userInteraction && !notification.localNotification) { + EventEmitter.emit('close_channel_drawer'); + if (app.startAppFromPushNotification) { + Client4.setToken(token); + Client4.setUrl(stripTrailingSlashes(url)); + } + + InteractionManager.runAfterInteractions(async () => { + await loadFromPushNotification(notification)(dispatch, getState); + + if (app.startAppFromPushNotification) { + app.launchApp(); + } else { + EventEmitter.emit(ViewTypes.NOTIFICATION_TAPPED); + } + }); + } +}; + +export const onPushNotificationReply = (data, text, badge, completed) => { + const {dispatch, getState} = store; + const state = getState(); + const {currentUserId: reduxCurrentUserId} = state.entities.users; + const reduxCredentialsUrl = state.entities.general.credentials.url; + const reduxCredentialsToken = state.entities.general.credentials.token; + + const currentUserId = reduxCurrentUserId || app.currentUserId; + const url = reduxCredentialsUrl || app.url; + const token = reduxCredentialsToken || app.token; + + if (currentUserId) { + // one thing to note is that for android it will reply to the last post in the stack + const rootId = data.root_id || data.post_id; + const post = { + user_id: currentUserId, + channel_id: data.channel_id, + root_id: rootId, + parent_id: rootId, + message: text, + }; + + if (!Client4.getUrl()) { + // Make sure the Client has the server url set + Client4.setUrl(url); + } + + if (!Client4.getToken()) { + // Make sure the Client has the server token set + Client4.setToken(token); + } + + createPost(post)(dispatch, getState).then(() => { + markChannelAsRead(data.channel_id)(dispatch, getState); + + if (badge >= 0) { + PushNotifications.setApplicationIconBadgeNumber(badge); + } + + app.setReplyNotificationData(null); + }).then(completed); + } else { + app.setReplyNotificationData({ + data, + text, + badge, + completed, + }); + } +}; + +export const configurePushNotifications = () => { + PushNotifications.configure({ + onRegister: onRegisterDevice, + onNotification: onPushNotification, + onReply: onPushNotificationReply, + popInitialNotification: true, + requestPermissions: true, + }); + + if (app) { + app.setIsNotificationsConfigured(true); + } +}; diff --git a/index.js b/index.js index b60030438..bd56c7a9f 100644 --- a/index.js +++ b/index.js @@ -10,5 +10,3 @@ import ShareExtension from 'share_extension/android'; if (Platform.OS === 'android') { AppRegistry.registerComponent('MattermostShare', () => ShareExtension); } - -const app = new Mattermost(); diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index c390b0126..e59e502e5 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; }; 62A8448264674B4D95A5A7C2 /* OpenSans-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C78A387124874496AD2C1466 /* OpenSans-Semibold.ttf */; }; 69AC753E496743BABB7A7124 /* OpenSans-SemiboldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0E617BF0F36D4E738F51D169 /* OpenSans-SemiboldItalic.ttf */; }; + 74D116AD208A8D5600CF8A79 /* libRNKeychain.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 74D116AA208A8D3100CF8A79 /* libRNKeychain.a */; }; 7F2691A11EE1DC6A007574FE /* libRNNotifications.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F2691A01EE1DC51007574FE /* libRNNotifications.a */; }; 7F292A711E8AB73400A450A3 /* SplashScreenResource in Resources */ = {isa = PBXBuildFile; fileRef = 7F292A701E8AB73400A450A3 /* SplashScreenResource */; }; 7F292AA61E8ABB1100A450A3 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */; }; @@ -367,6 +368,20 @@ remoteGlobalIDString = 2D2A28201D9B03D100D4039D; remoteInfo = "RCTAnimation-tvOS"; }; + 74D116A9208A8D3100CF8A79 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 74D116A4208A8D3000CF8A79 /* RNKeychain.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 5D82366F1B0CE05B005A9EF3; + remoteInfo = RNKeychain; + }; + 74D116AB208A8D3100CF8A79 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 74D116A4208A8D3000CF8A79 /* RNKeychain.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 6478985F1F38BF9100DA1C12; + remoteInfo = "RNKeychain-tvOS"; + }; 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; @@ -691,6 +706,8 @@ 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Light.ttf"; path = "../assets/fonts/OpenSans-Light.ttf"; sourceTree = ""; }; 65FD5EA57EBAE06106094B2F /* libPods-Mattermost.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Mattermost.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 6BAF8296411D4657B5A0E8F8 /* libRNReactNativeDocViewer.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNReactNativeDocViewer.a; sourceTree = ""; }; + 74D116A3208A8C8400CF8A79 /* libRNKeychain.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libRNKeychain.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 74D116A4208A8D3000CF8A79 /* RNKeychain.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNKeychain.xcodeproj; path = "../node_modules/react-native-keychain/RNKeychain.xcodeproj"; sourceTree = ""; }; 7535D128F00C4A47A182627E /* libRNCookieManagerIOS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCookieManagerIOS.a; sourceTree = ""; }; 77810F0A063349439B0D8B6B /* libJailMonkey.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libJailMonkey.a; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; @@ -778,6 +795,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 74D116AD208A8D5600CF8A79 /* libRNKeychain.a in Frameworks */, 37ABD3C81F4CE142001FDE6B /* libART.a in Frameworks */, 375218501F4B9EE70035444B /* libRCTCameraRoll.a in Frameworks */, 7F43D5E01F6BF994001FC614 /* libRNSVG.a in Frameworks */, @@ -1077,6 +1095,7 @@ 4B992D7BAAEDF8759DB525B5 /* Frameworks */ = { isa = PBXGroup; children = ( + 74D116A3208A8C8400CF8A79 /* libRNKeychain.a */, 7FFE32B51FD9CCAA0038C7A0 /* FLAnimatedImage.framework */, 7FFE32B61FD9CCAA0038C7A0 /* KSCrash.framework */, 7FFE32B71FD9CCAA0038C7A0 /* KSCrash.framework */, @@ -1116,6 +1135,15 @@ name = Products; sourceTree = ""; }; + 74D116A5208A8D3000CF8A79 /* Products */ = { + isa = PBXGroup; + children = ( + 74D116AA208A8D3100CF8A79 /* libRNKeychain.a */, + 74D116AC208A8D3100CF8A79 /* libRNKeychain.a */, + ); + name = Products; + sourceTree = ""; + }; 78C398B11ACF4ADC00677621 /* Products */ = { isa = PBXGroup; children = ( @@ -1319,6 +1347,7 @@ children = ( 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */, 7F642DE72093530B00F3165E /* RNDeviceInfo.xcodeproj */, + 74D116A4208A8D3000CF8A79 /* RNKeychain.xcodeproj */, 7FD8DE972029ECDE001AAC5E /* RNTableView.xcodeproj */, 7F7D7F52201645D300D31155 /* ReactNativePermissions.xcodeproj */, 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */, @@ -1631,6 +1660,10 @@ ProductGroup = 7FDB92761F706F45006CDFD1 /* Products */; ProjectRef = 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */; }, + { + ProductGroup = 74D116A5208A8D3000CF8A79 /* Products */; + ProjectRef = 74D116A4208A8D3000CF8A79 /* RNKeychain.xcodeproj */; + }, { ProductGroup = 7F8C49621F3DFC30003A22BA /* Products */; ProjectRef = D0281D64B98143668D6AD42B /* RNLocalAuth.xcodeproj */; @@ -1927,6 +1960,20 @@ remoteRef = 5E9157341DD0AC6500FF2AA8 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 74D116AA208A8D3100CF8A79 /* libRNKeychain.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNKeychain.a; + remoteRef = 74D116A9208A8D3100CF8A79 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 74D116AC208A8D3100CF8A79 /* libRNKeychain.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNKeychain.a; + remoteRef = 74D116AB208A8D3100CF8A79 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/package-lock.json b/package-lock.json index c87ac1208..2402e4d41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14418,9 +14418,7 @@ } }, "react-native-device-info": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-0.21.5.tgz", - "integrity": "sha512-Bvl7TyVMDbBH3wzvZx0xj3deMhWVRDHdOdjP1MjGVxVVH+bSlCi6oYDScaX2yz5rfi2OvevWWCg8jUqFKeepjA==" + "version": "github:enahum/react-native-device-info#27ecfd4a59341bf6ed71d11203d914067070e8df" }, "react-native-dismiss-keyboard": { "version": "1.0.0", @@ -14476,7 +14474,7 @@ } }, "react-native-fetch-blob": { - "version": "github:enahum/react-native-fetch-blob#75d5abfa1886665d7eaa947e70b9297b981f0983", + "version": "github:enahum/react-native-fetch-blob#2e8f9b05b012496bd3ff9790aace3f2b462e2fee", "requires": { "base-64": "0.1.0", "glob": "7.0.6" @@ -14524,6 +14522,11 @@ "react-native-iphone-x-helper": "1.0.2" } }, + "react-native-keychain": { + "version": "3.0.0-rc.3", + "resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-3.0.0-rc.3.tgz", + "integrity": "sha512-ijWfHmxTPKnrHtPJiDbKW3D6lRH8O9wbCNEE3xlxEg1WZT+VhP6iiF+HUansNYuxL7Hh7k41GSFfvr3xumfmXA==" + }, "react-native-linear-gradient": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.4.0.tgz", diff --git a/package.json b/package.json index b3b220dd1..c2cf18647 100644 --- a/package.json +++ b/package.json @@ -25,16 +25,17 @@ "react-native-button": "2.3.0", "react-native-circular-progress": "0.2.0", "react-native-cookies": "3.2.0", - "react-native-device-info": "0.21.5", + "react-native-device-info": "enahum/react-native-device-info.git#27ecfd4a59341bf6ed71d11203d914067070e8df", "react-native-doc-viewer": "2.7.8", "react-native-document-picker": "2.1.0", "react-native-drawer": "2.5.0", "react-native-exception-handler": "2.7.5", "react-native-fast-image": "4.0.8", - "react-native-fetch-blob": "enahum/react-native-fetch-blob.git#75d5abfa1886665d7eaa947e70b9297b981f0983", + "react-native-fetch-blob": "enahum/react-native-fetch-blob.git#2e8f9b05b012496bd3ff9790aace3f2b462e2fee", "react-native-image-gallery": "enahum/react-native-image-gallery#a98b1051e94f5a394541ca1ff9b15e2c9ffed84f", "react-native-image-picker": "0.26.7", "react-native-keyboard-aware-scroll-view": "0.5.0", + "react-native-keychain": "3.0.0-rc.3", "react-native-linear-gradient": "2.4.0", "react-native-local-auth": "enahum/react-native-local-auth.git#cc9ce2f468fbf7b431dfad3191a31aaa9227a6ab", "react-native-navigation": "1.1.450",