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
This commit is contained in:
Chris Duarte 2018-05-18 14:13:00 -07:00 committed by Elias Nahum
parent 21f2624bec
commit e8712f9199
30 changed files with 1883 additions and 702 deletions

View file

@ -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; \

View file

@ -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"])

View file

@ -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<String, Object> getConstants() {
Map<String, Object> 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<String> keys = new ArrayList<String>(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<String, String> 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;
}
}

View file

@ -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<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new InitializationModule(mApplication, reactContext));
}
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View file

@ -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)
);
}

View file

@ -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",

View file

@ -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<String, String> multiGet(ReadableArray keys) {
HashMap<String, String> results = new HashMap<>(keys.size());
HashSet<String> 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;
}
}

View file

@ -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<Object> toArrayList() {
return null;
}
}

View file

@ -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) {
}
}

View file

@ -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;
}
}

View file

@ -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<String, Object> getConstants() {
HashMap<String, Object> constants = new HashMap<>(1);
constants.put("isOpened", mApplication.sharedExtensionIsOpened);
mApplication.sharedExtensionIsOpened = false;
return constants;
}
@ReactMethod
public void close(ReadableMap data) {
this.clear();

View file

@ -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<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new ShareModule(reactContext));
return Arrays.<NativeModule>asList(new ShareModule(mApplication, reactContext));
}
public List<Class<? extends JavaScriptModule>> createJSModules() {

View file

@ -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'

View file

@ -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;

290
app/app.js Normal file
View file

@ -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);
}
}

View file

@ -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 (
<View style={[style.header, padding, {height}]}>
<View style={style.button_container}>
<View style={style.button_wrapper}>
<Icon
name='md-menu'
size={25}
color={theme.sidebarHeaderTextColor}
/>
</View>
</View>
</View>
);
}
}
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,
},
};
});

View file

@ -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';

View file

@ -64,6 +64,9 @@ const ViewTypes = keyMirror({
ANNOUNCEMENT_BANNER: null,
INCREMENT_EMOJI_PICKER_PAGE: null,
LAUNCH_LOGIN: null,
LAUNCH_CHANNEL: null,
});
export default {

File diff suppressed because it is too large Load diff

View file

@ -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 = {

274
app/screens/entry/entry.js Normal file
View file

@ -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 (
<SelectServer
navigator={this.props.navigator}
allowOtherServers={app.allowOtherServers}
/>
);
};
renderChannel = () => {
const ChannelScreen = lazyLoadChannel();
return (
<ChannelScreen
navigator={this.props.navigator}
/>
);
};
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 = (
<View>
<StatusBar headerColor={app.toolbarBackground}/>
<EmptyToolbar
theme={toolbarTheme}
isLandscape={isLandscape}
/>
</View>
);
}
return (
<SafeAreaView
navBarBackgroundColor={app.toolbarBackground}
backgroundColor={backgroundColor}
navigator={navigator}
>
{toolbar}
<Loading/>
</SafeAreaView>
);
}
}

View file

@ -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());

View file

@ -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
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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);
}
};

View file

@ -10,5 +10,3 @@ import ShareExtension from 'share_extension/android';
if (Platform.OS === 'android') {
AppRegistry.registerComponent('MattermostShare', () => ShareExtension);
}
const app = new Mattermost();

View file

@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
7535D128F00C4A47A182627E /* libRNCookieManagerIOS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCookieManagerIOS.a; sourceTree = "<group>"; };
77810F0A063349439B0D8B6B /* libJailMonkey.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libJailMonkey.a; sourceTree = "<group>"; };
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
@ -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 = "<group>";
};
74D116A5208A8D3000CF8A79 /* Products */ = {
isa = PBXGroup;
children = (
74D116AA208A8D3100CF8A79 /* libRNKeychain.a */,
74D116AC208A8D3100CF8A79 /* libRNKeychain.a */,
);
name = Products;
sourceTree = "<group>";
};
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;

11
package-lock.json generated
View file

@ -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",

View file

@ -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",