diff --git a/CHANGELOG.md b/CHANGELOG.md index 877e7cfb3..da70323d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Mattermost Mobile Apps Changelog +## 1.20.1 Release +- Release Date: June 21, 2019 +- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device + +### Combatibility + - Mobile App v1.13+ is required for Mattermost Server v5.4+. + - Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html). + - iPhone 5s devices and later with iOS 11+ is required. + +### Bug Fixes + - Fixed an issue where some Android devices were crashing. + - Fixed an issue where messages were missing after reconnecting the network. + ## 1.20.0 Release - Release Date: June 16, 2019 - Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device diff --git a/README.md b/README.md index 244fadf6e..6afea1902 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ To help with testing app updates before they're released, you can: ### Contribute Code -1. Look in [GitHub issues](https://github.com/mattermost/mattermost-server/issues?q=label%3A"React+Native") for issues marked as [Help Wanted] +1. Look in [GitHub issues](https://mattermost.com/pl/help-wanted-mattermost-mobile) for issues marked as [Help Wanted] 2. Comment to let people know you’re working on it 3. Follow [these instructions](https://developers.mattermost.com/contribute/mobile/developer-setup/) to set up your developer environment 4. Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions diff --git a/android/app/build.gradle b/android/app/build.gradle index bb876b35d..20028c302 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -114,8 +114,8 @@ android { } packagingOptions { - pickFirst 'lib/x86_64/libjsc.so' - pickFirst 'lib/arm64-v8a/libjsc.so' + pickFirst '**/libjsc.so' + pickFirst '**/libc++_shared.so' } defaultConfig { @@ -123,8 +123,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5" - versionCode 198 - versionName "1.20.0" + versionCode 203 + versionName "1.21.0" multiDexEnabled = true ndk { abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64' @@ -194,9 +194,6 @@ repositories { configurations.all { resolutionStrategy { eachDependency { DependencyResolveDetails details -> - if (details.requested.name == 'android-jsc') { - details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r241213' - } if (details.requested.name == 'play-services-base') { details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1' } @@ -214,6 +211,9 @@ configurations.all { } dependencies { + // Make sure to put android-jsc at the top + implementation "org.webkit:android-jsc-intl:r241213" + implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" implementation 'com.android.support:design:28.0.0' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e3d966fce..4fff02aa5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -44,7 +44,8 @@ android:exported="false" /> + android:configChanges="keyboard|keyboardHidden|orientation|screenSize" + android:resizeableActivity="true"/> 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 - * - * 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); - 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); - - 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 (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 deleted file mode 100644 index 79e7e12e7..000000000 --- a/android/app/src/main/java/com/mattermost/rnbeta/InitializationPackage.java +++ /dev/null @@ -1,36 +0,0 @@ -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 8521c9efb..c089baeee 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -62,7 +62,6 @@ public class MainApplication extends NavigationApplication implements INotificat public static MainApplication instance; public Boolean sharedExtensionIsOpened = false; - public Boolean replyFromPushNotification = false; public long APP_START_TIME; @@ -116,7 +115,6 @@ public class MainApplication extends NavigationApplication implements INotificat new ReactNativeDocumentPicker(), new SharePackage(this), new KeychainPackage(), - new InitializationPackage(this), new AsyncStoragePackage(), new NetInfoPackage(), new RNCWebViewPackage(), diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostCredentialsHelper.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostCredentialsHelper.java new file mode 100644 index 000000000..1c1d331ee --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostCredentialsHelper.java @@ -0,0 +1,41 @@ +package com.mattermost.rnbeta; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.HashMap; + +import com.facebook.react.bridge.ReactApplicationContext; + +import com.oblador.keychain.KeychainModule; + +import com.mattermost.react_native_interface.ResolvePromise; +import com.mattermost.react_native_interface.AsyncStorageHelper; +import com.mattermost.react_native_interface.KeysReadableArray; + +public class MattermostCredentialsHelper { + static final String CURRENT_SERVER_URL = "@currentServerUrl"; + + public static void getCredentialsForCurrentServer(ReactApplicationContext context, ResolvePromise promise) { + final KeychainModule keychainModule = new KeychainModule(context); + final AsyncStorageHelper asyncStorage = new AsyncStorageHelper(context); + final ArrayList keys = new ArrayList(1); + keys.add(CURRENT_SERVER_URL); + KeysReadableArray asyncStorageKeys = new KeysReadableArray() { + @Override + public int size() { + return keys.size(); + } + + @Override + public String getString(int index) { + return keys.get(index); + } + }; + + HashMap asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys); + String serverUrl = asyncStorageResults.get(CURRENT_SERVER_URL); + + keychainModule.getGenericPasswordForOptions(serverUrl, promise); + } +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java index 1e73f5e9c..68ecbec2a 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java @@ -13,6 +13,7 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.WritableMap; public class MattermostManagedModule extends ReactContextBaseJavaModule { private static MattermostManagedModule instance; @@ -73,6 +74,19 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule { System.exit(0); } + @ReactMethod + public void isRunningInSplitView(final Promise promise) { + WritableMap result = Arguments.createMap(); + Activity current = getCurrentActivity(); + if (current != null) { + result.putBoolean("isSplitView", current.isInMultiWindowMode()); + } else { + result.putBoolean("isSplitView", false); + } + + promise.resolve(result); + } + @ReactMethod public void quitApp() { getCurrentActivity().finish(); diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java index 2d69a3b2d..dd02b935e 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java @@ -24,7 +24,6 @@ import okhttp3.Response; import com.mattermost.react_native_interface.ResolvePromise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.WritableMap; -import com.oblador.keychain.KeychainModule; import com.wix.reactnativenotifications.core.NotificationIntentAdapter; @@ -36,34 +35,30 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + final CharSequence message = getReplyMessage(intent); + if (message == null) { + return; + } + mContext = context; bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context); final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1); - final CharSequence message = getReplyMessage(intent); - final KeychainModule keychainModule = new KeychainModule(reactApplicationContext); - keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() { + + MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() { @Override public void resolve(@Nullable Object value) { if (value instanceof Boolean && !(Boolean)value) { - String channelId = bundle.getString("channel_id"); - onReplyFailed(notificationManager, notificationId, channelId); return; } WritableMap map = (WritableMap) value; if (map != null) { - String[] credentials = map.getString("password").split(",[ ]*"); - String token = null; - String serverUrl = null; - if (credentials.length == 2) { - token = credentials[0]; - serverUrl = credentials[1]; - - } + String token = map.getString("password"); + String serverUrl = map.getString("service"); Log.i("ReactNative", String.format("URL=%s TOKEN=%s", serverUrl, token)); replyToMessage(serverUrl, token, notificationId, message); @@ -75,7 +70,11 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver { protected void replyToMessage(final String serverUrl, final String token, final int notificationId, final CharSequence message) { final String channelId = bundle.getString("channel_id"); - final String rootId = bundle.getString("post_id"); + final String postId = bundle.getString("post_id"); + String rootId = bundle.getString("root_id"); + if (android.text.TextUtils.isEmpty(rootId)) { + rootId = postId; + } if (token == null || serverUrl == null) { onReplyFailed(notificationManager, notificationId, channelId); diff --git a/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java b/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java index 64ec0d96f..a276fec0a 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java @@ -15,17 +15,18 @@ import okhttp3.Response; import org.json.JSONObject; import org.json.JSONException; -import com.mattermost.react_native_interface.ResolvePromise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.WritableMap; -import com.oblador.keychain.KeychainModule; + +import com.mattermost.react_native_interface.ResolvePromise; public class ReceiptDelivery { + static final String CURRENT_SERVER_URL = "@currentServerUrl"; + public static void send (Context context, final String ackId, final String type) { final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context); - final KeychainModule keychainModule = new KeychainModule(reactApplicationContext); - keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() { + MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() { @Override public void resolve(@Nullable Object value) { if (value instanceof Boolean && !(Boolean)value) { @@ -34,14 +35,8 @@ public class ReceiptDelivery { WritableMap map = (WritableMap) value; if (map != null) { - String[] credentials = map.getString("password").split(",[ ]*"); - String token = null; - String serverUrl = null; - if (credentials.length == 2) { - token = credentials[0]; - serverUrl = credentials[1]; - - } + String token = map.getString("password"); + String serverUrl = map.getString("service"); Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with TOKEN=%s", ackId, type, serverUrl, token)); execute(serverUrl, token, ackId, type); diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 511f5a8b6..687d1178d 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -21,7 +21,7 @@ import { } from 'mattermost-redux/actions/posts'; import {getFilesForPost} from 'mattermost-redux/actions/files'; import {savePreferences} from 'mattermost-redux/actions/preferences'; -import {getTeamMembersByIds} from 'mattermost-redux/actions/teams'; +import {getTeamMembersByIds, selectTeam} from 'mattermost-redux/actions/teams'; import {getProfilesInChannel} from 'mattermost-redux/actions/users'; import {General, Preferences} from 'mattermost-redux/constants'; import {getPostIdsInChannel} from 'mattermost-redux/selectors/entities/posts'; @@ -54,8 +54,8 @@ import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels' const MAX_POST_TRIES = 3; export function loadChannelsIfNecessary(teamId) { - return async (dispatch, getState) => { - await fetchMyChannelsAndMembers(teamId)(dispatch, getState); + return async (dispatch) => { + await dispatch(fetchMyChannelsAndMembers(teamId)); }; } @@ -198,7 +198,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) { }); } } else { - const {lastConnectAt} = state.device.websocket; + const {lastConnectAt} = state.websocket; const lastGetPosts = state.views.channel.lastGetPosts[channelId]; let since; @@ -416,6 +416,12 @@ export function handleSelectChannelByName(channelName, teamName) { const currentTeamName = currentTeam?.name; const {data: channel} = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName)); const currentChannelId = getCurrentChannelId(state); + + if (teamName && teamName !== currentTeamName) { + const team = getTeamByName(state, teamName); + dispatch(selectTeam(team)); + } + if (channel && currentChannelId !== channel.id) { dispatch(handleSelectChannel(channel.id)); } diff --git a/app/actions/views/login.js b/app/actions/views/login.js index 740659e4d..f84d97b9d 100644 --- a/app/actions/views/login.js +++ b/app/actions/views/login.js @@ -11,7 +11,7 @@ import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {ViewTypes} from 'app/constants'; -import {app} from 'app/mattermost'; +import {setAppCredentials} from 'app/init/credentials'; import PushNotifications from 'app/push_notifications'; import {getDeviceTimezone} from 'app/utils/timezone'; import {setCSRFFromCookie} from 'app/utils/security'; @@ -45,7 +45,7 @@ export function handleSuccessfulLogin() { const currentUserId = getCurrentUserId(state); await setCSRFFromCookie(url); - app.setAppCredentials(deviceToken, currentUserId, token, url); + setAppCredentials(deviceToken, currentUserId, token, url); const enableTimezone = isTimezoneEnabled(state); if (enableTimezone) { @@ -56,7 +56,6 @@ export function handleSuccessfulLogin() { type: GeneralTypes.RECEIVED_APP_CREDENTIALS, data: { url, - token, }, }); diff --git a/app/actions/views/login.test.js b/app/actions/views/login.test.js index 66020d79c..f75dee2f2 100644 --- a/app/actions/views/login.test.js +++ b/app/actions/views/login.test.js @@ -11,10 +11,8 @@ import { handlePasswordChanged, } from 'app/actions/views/login'; -jest.mock('app/mattermost', () => ({ - app: { - setAppCredentials: () => jest.fn(), - }, +jest.mock('app/init/credentials', () => ({ + setAppCredentials: () => jest.fn(), })); jest.mock('react-native-cookies', () => ({ diff --git a/app/app.js b/app/app.js deleted file mode 100644 index ec896bc18..000000000 --- a/app/app.js +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/* eslint-disable global-require*/ -import {Linking, NativeModules, Platform, Text} from 'react-native'; -import AsyncStorage from '@react-native-community/async-storage'; -import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain'; - -import {loadMe} from 'mattermost-redux/actions/users'; -import {Client4} from 'mattermost-redux/client'; - -import {resetToChannel, resetToSelectServer} from 'app/actions/navigation'; -import {setDeepLinkURL} from 'app/actions/views/root'; -import tracker from 'app/utils/time_tracker'; -import {getCurrentLocale} from 'app/selectors/i18n'; - -import {getTranslations as getLocalTranslations} from 'app/i18n'; -import {store, handleManagedConfig, initializeModules} from 'app/mattermost'; -import avoidNativeBridge from 'app/utils/avoid_native_bridge'; -import {setCSRFFromCookie} from 'app/utils/security'; - -const {Initialization} = NativeModules; - -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; - this.previousAppState = null; - - // Usage: screen/entry.js - this.startAppFromPushNotification = false; - this.isNotificationsConfigured = false; - this.allowOtherServers = true; - this.canLaunchEntry = true; - this.emmEnabled = false; - this.performingEMMAuthentication = 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; - - // Usage deeplinking - Linking.addEventListener('url', this.handleDeepLink); - - this.setFontFamily(); - this.getStartupThemes(); - this.getAppCredentials(); - } - - setFontFamily = () => { - // Set a global font for Android - if (Platform.OS === 'android') { - const defaultFontFamily = { - style: { - fontFamily: 'Roboto', - }, - }; - const TextRender = Text.render; - const initialDefaultProps = Text.defaultProps; - Text.defaultProps = { - ...initialDefaultProps, - ...defaultFontFamily, - }; - Text.render = function render(props, ...args) { - const oldProps = props; - let newProps = {...props, style: [defaultFontFamily.style, props.style]}; - try { - return Reflect.apply(TextRender, this, [newProps, ...args]); - } finally { - newProps = oldProps; - } - }; - } - }; - - getTranslations = () => { - if (this.translations) { - return this.translations; - } - - const state = store.getState(); - const locale = getCurrentLocale(state); - - this.translations = getLocalTranslations(locale); - return this.translations; - }; - - getAppCredentials = async () => { - try { - const credentials = await avoidNativeBridge( - () => { - return Initialization.credentialsExist; - }, - () => { - return Initialization.credentials; - }, - () => { - this.waitForRehydration = true; - 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; - - // if for any case the url and the token aren't valid proceed with re-hydration - if (url && url !== 'undefined' && token && token !== 'undefined') { - this.deviceToken = deviceToken; - this.currentUserId = currentUserId; - this.token = token; - this.url = url; - Client4.setUrl(url); - Client4.setToken(token); - await setCSRFFromCookie(url); - } else { - this.waitForRehydration = true; - } - } - } else { - this.waitForRehydration = false; - } - } 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; - }; - - setPerformingEMMAuthentication = (authenticating) => { - this.performingEMMAuthentication = authenticating; - }; - - setAppCredentials = (deviceToken, currentUserId, token, url) => { - if (!currentUserId) { - return; - } - - const username = `${deviceToken}, ${currentUserId}`; - const password = `${token},${url}`; - - if (this.waitForRehydration) { - this.waitForRehydration = false; - this.token = token; - this.url = url; - } - - // Only save to keychain if the url and token are set - if (url && token) { - try { - setGenericPassword(username, password); - } catch (e) { - console.warn('could not set credentials', e); //eslint-disable-line no-console - } - } - }; - - 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([ - TOOLBAR_BACKGROUND, - TOOLBAR_TEXT_COLOR, - APP_BACKGROUND, - ]); - }; - - handleDeepLink = (event) => { - const {url} = event; - store.dispatch(setDeepLinkURL(url)); - } - - launchApp = async () => { - const shouldStart = await handleManagedConfig(); - if (shouldStart) { - this.startApp(); - } - }; - - startApp = () => { - if (this.waitForRehydration) { - return; - } - - const {dispatch} = store; - - Linking.getInitialURL().then((url) => { - dispatch(setDeepLinkURL(url)); - }); - - let screen = 'SelectServer'; - if (this.token && this.url) { - screen = 'Channel'; - tracker.initialLoad = Date.now(); - - try { - dispatch(loadMe()); - } catch (e) { - // Fall through since we should have a previous version of the current user because we have a token - console.warn('Failed to load current user when starting on Channel screen', e); // eslint-disable-line no-console - } - } - - switch (screen) { - case 'SelectServer': - dispatch(resetToSelectServer(this.allowOtherServers)); - break; - case 'Channel': - dispatch(resetToChannel()); - break; - } - - initializeModules(); - - this.setAppStarted(true); - } -} diff --git a/app/components/message_attachments/action_button/action_button.js b/app/components/message_attachments/action_button/action_button.js index 86374fd04..a604f6995 100644 --- a/app/components/message_attachments/action_button/action_button.js +++ b/app/components/message_attachments/action_button/action_button.js @@ -12,17 +12,18 @@ import ActionButtonText from './action_button_text'; export default class ActionButton extends PureComponent { static propTypes = { actions: PropTypes.shape({ - doPostAction: PropTypes.func.isRequired, + doPostActionWithCookie: PropTypes.func.isRequired, }).isRequired, id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, postId: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, + cookie: PropTypes.string.isRequired, }; handleActionPress = preventDoubleTap(() => { - const {actions, id, postId} = this.props; - actions.doPostAction(postId, id); + const {actions, id, postId, cookie} = this.props; + actions.doPostActionWithCookie(postId, id, cookie); }, 4000); render() { diff --git a/app/components/message_attachments/action_button/index.js b/app/components/message_attachments/action_button/index.js index 7b87164f8..fc7e7cec3 100644 --- a/app/components/message_attachments/action_button/index.js +++ b/app/components/message_attachments/action_button/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {doPostAction} from 'mattermost-redux/actions/posts'; +import {doPostActionWithCookie} from 'mattermost-redux/actions/posts'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import ActionButton from './action_button'; @@ -18,7 +18,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - doPostAction, + doPostActionWithCookie, }, dispatch), }; } diff --git a/app/components/message_attachments/attachment_actions.js b/app/components/message_attachments/attachment_actions.js index 67e77886e..9f4203fbd 100644 --- a/app/components/message_attachments/attachment_actions.js +++ b/app/components/message_attachments/attachment_actions.js @@ -50,6 +50,7 @@ export default class AttachmentActions extends PureComponent { diff --git a/app/components/post_attachment_opengraph/post_attachment_opengraph.js b/app/components/post_attachment_opengraph/post_attachment_opengraph.js index 9ae9ace99..cb7d413ef 100644 --- a/app/components/post_attachment_opengraph/post_attachment_opengraph.js +++ b/app/components/post_attachment_opengraph/post_attachment_opengraph.js @@ -89,6 +89,7 @@ export default class PostAttachmentOpenGraph extends PureComponent { const bestImage = getNearestPoint(bestDimensions, data.images, 'width', 'height'); const imageUrl = bestImage.secure_url || bestImage.url; + let ogImage; if (imagesMetadata && imagesMetadata[imageUrl]) { ogImage = imagesMetadata[imageUrl]; @@ -98,6 +99,12 @@ export default class PostAttachmentOpenGraph extends PureComponent { ogImage = data.images.find((i) => i.url === imageUrl || i.secure_url === imageUrl); } + // Fallback when the ogImage does not have dimensions but there is a metaImage defined + const metaImages = imagesMetadata ? Object.values(imagesMetadata) : null; + if ((!ogImage?.width || !ogImage?.height) && metaImages?.length) { + ogImage = metaImages[0]; + } + let dimensions = bestDimensions; if (ogImage?.width && ogImage?.height) { dimensions = calculateDimensions(ogImage.height, ogImage.width, this.getViewPostWidth()); @@ -139,9 +146,16 @@ export default class PostAttachmentOpenGraph extends PureComponent { ogImage = openGraphData.images.find((i) => i.url === openGraphImageUrl || i.secure_url === openGraphImageUrl); } + // Fallback when the ogImage does not have dimensions but there is a metaImage defined + const metaImages = imagesMetadata ? Object.values(imagesMetadata) : null; + if ((!ogImage?.width || !ogImage?.height) && metaImages?.length) { + ogImage = metaImages[0]; + } + if (ogImage?.width && ogImage?.height) { this.setImageSize(imageUrl, ogImage.width, ogImage.height); } else { + // if we get to this point there can be a scroll pop Image.getSize(imageUrl, (width, height) => { this.setImageSize(imageUrl, width, height); }, () => null); diff --git a/app/components/post_body/index.js b/app/components/post_body/index.js index 01c0de74a..5d4b9ef26 100644 --- a/app/components/post_body/index.js +++ b/app/components/post_body/index.js @@ -11,6 +11,7 @@ import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis'; +import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts'; import {memoizeResult} from 'mattermost-redux/utils/helpers'; import { @@ -33,10 +34,12 @@ const POST_TIMEOUT = 20000; function makeMapStateToProps() { const memoizeHasEmojisOnly = memoizeResult((message, customEmojis) => hasEmojisOnly(message, customEmojis)); + const getReactionsForPost = makeGetReactionsForPost(); return (state, ownProps) => { const post = ownProps.post; const channel = getChannel(state, post.channel_id) || {}; + const reactions = getReactionsForPost(state, post.id); let isFailed = post.failed; let isPending = post.id === post.pending_post_id; @@ -86,7 +89,7 @@ function makeMapStateToProps() { fileIds: post.file_ids, hasBeenDeleted: post.state === Posts.POST_DELETED, hasBeenEdited: isEdited(post), - hasReactions: post.has_reactions, + hasReactions: (reactions && Object.keys(reactions).length > 0) || Boolean(post.has_reactions), isFailed, isPending, isPostAddChannelMember, diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js index 80c925615..f5b311d16 100644 --- a/app/components/post_body_additional_content/post_body_additional_content.js +++ b/app/components/post_body_additional_content/post_body_additional_content.js @@ -10,10 +10,13 @@ import { Platform, StyleSheet, TouchableOpacity, + StatusBar, } from 'react-native'; import {YouTubeStandaloneAndroid, YouTubeStandaloneIOS} from 'react-native-youtube'; import {intlShape} from 'react-intl'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; + import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout'; import PostAttachmentImage from 'app/components/post_attachment_image'; import ProgressiveImage from 'app/components/progressive_image'; @@ -179,11 +182,12 @@ export default class PostBodyAdditionalContent extends PureComponent { }; generateStaticEmbed = (isYouTube, isImage) => { - if (isYouTube || isImage) { + const {isReplyPost, link, metadata, openGraphData, showLinkPreviews, theme} = this.props; + + if (isYouTube || (isImage && !openGraphData)) { return null; } - const {isReplyPost, link, metadata, openGraphData, showLinkPreviews, theme} = this.props; const attachments = this.getMessageAttachment(); if (attachments) { return attachments; @@ -439,6 +443,7 @@ export default class PostBodyAdditionalContent extends PureComponent { if (Platform.OS === 'ios') { YouTubeStandaloneIOS. playVideo(videoId, startTime). + then(this.playYouTubeVideoEnded). catch(this.playYouTubeVideoError); } else { const {googleDeveloperKey} = this.props; @@ -456,6 +461,13 @@ export default class PostBodyAdditionalContent extends PureComponent { } }; + playYouTubeVideoEnded = () => { + if (Platform.OS === 'ios') { + StatusBar.setHidden(false); + EventEmitter.emit('update_safe_area_view'); + } + }; + playYouTubeVideoError = (errorMessage) => { const {formatMessage} = this.context.intl; diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 39e16dcac..262c0d373 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -280,7 +280,9 @@ export default class PostList extends PureComponent { scrollToBottom = () => { setTimeout(() => { - this.flatListRef.current.scrollToOffset({offset: 0, animated: true}); + if (this.flatListRef && this.flatListRef.current) { + this.flatListRef.current.scrollToOffset({offset: 0, animated: true}); + } }, 250); }; @@ -291,11 +293,13 @@ export default class PostList extends PureComponent { this.props.initialIndex > 0 && !this.hasDoneInitialScroll ) { - this.flatListRef.current.scrollToIndex({ - animated: false, - index: this.props.initialIndex, - viewOffset: 50, - viewPosition: 0.5, + requestAnimationFrame(() => { + this.flatListRef.current.scrollToIndex({ + animated: false, + index: this.props.initialIndex, + viewOffset: 0, + viewPosition: 1, // 0 is at bottom + }); }); this.hasDoneInitialScroll = true; } diff --git a/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap b/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap new file mode 100644 index 000000000..ac7535189 --- /dev/null +++ b/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap @@ -0,0 +1,149 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PostTextBox should match, full snapshot 1`] = ` + + + + + + + + + + + + +`; diff --git a/app/components/post_textbox/post_textbox.test.js b/app/components/post_textbox/post_textbox.test.js new file mode 100644 index 000000000..6362f1397 --- /dev/null +++ b/app/components/post_textbox/post_textbox.test.js @@ -0,0 +1,90 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallowWithIntl} from 'test/intl-test-helper'; + +import Preferences from 'mattermost-redux/constants/preferences'; + +import PostTextbox from './post_textbox.ios'; + +jest.mock('NativeEventEmitter'); + +describe('PostTextBox', () => { + const baseProps = { + actions: { + addReactionToLatestPost: jest.fn(), + createPost: jest.fn(), + executeCommand: jest.fn(), + handleCommentDraftChanged: jest.fn(), + handlePostDraftChanged: jest.fn(), + handleClearFiles: jest.fn(), + handleClearFailedFiles: jest.fn(), + handleRemoveLastFile: jest.fn(), + initUploadFiles: jest.fn(), + userTyping: jest.fn(), + handleCommentDraftSelectionChanged: jest.fn(), + setStatus: jest.fn(), + selectPenultimateChannel: jest.fn(), + }, + canUploadFiles: true, + channelId: 'channel-id', + channelDisplayName: 'Test Channel', + channelTeamId: 'channel-team-id', + channelIsLoading: false, + channelIsReadOnly: false, + currentUserId: 'current-user-id', + deactivatedChannel: false, + files: [], + maxFileSize: 1024, + maxMessageLength: 4000, + navigator: { + showModal: jest.fn(), + }, + rootId: '', + theme: Preferences.THEMES.default, + uploadFileRequestStatus: 'NOT_STARTED', + value: '', + userIsOutOfOffice: false, + channelIsArchived: false, + onCloseChannel: jest.fn(), + cursorPositionEvent: '', + valueEvent: '', + }; + + test('should match, full snapshot', () => { + const wrapper = shallowWithIntl( + + ); + + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should emit the event but no text is save to draft', () => { + const wrapper = shallowWithIntl( + + ); + + wrapper.setState({value: 'some text'}); + + const instance = wrapper.instance(); + + instance.changeDraft = jest.fn(); + instance.handleAppStateChange('active'); + expect(instance.changeDraft).not.toBeCalled(); + }); + + test('should emit the event and text is save to draft', () => { + const wrapper = shallowWithIntl( + + ); + + const instance = wrapper.instance(); + const value = 'some text'; + + wrapper.setState({value}); + instance.handleAppStateChange('background'); + expect(baseProps.actions.handlePostDraftChanged).toHaveBeenCalledWith(baseProps.channelId, value); + expect(baseProps.actions.handlePostDraftChanged).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/components/post_textbox/post_textbox_base.js b/app/components/post_textbox/post_textbox_base.js index fdc5d12bc..f129c7d74 100644 --- a/app/components/post_textbox/post_textbox_base.js +++ b/app/components/post_textbox/post_textbox_base.js @@ -5,6 +5,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { Alert, + AppState, BackHandler, findNodeHandle, Keyboard, @@ -98,7 +99,10 @@ export default class PostTextBoxBase extends PureComponent { componentDidMount() { const event = this.props.rootId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT; + EventEmitter.on(event, this.handleInsertTextToDraft); + AppState.addEventListener('change', this.handleAppStateChange); + if (Platform.OS === 'android') { Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack); @@ -113,7 +117,10 @@ export default class PostTextBoxBase extends PureComponent { componentWillUnmount() { const event = this.props.rootId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT; + EventEmitter.off(event, this.handleInsertTextToDraft); + AppState.removeEventListener('change', this.handleAppStateChange); + if (Platform.OS === 'android') { Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack); @@ -246,6 +253,12 @@ export default class PostTextBoxBase extends PureComponent { return false; }; + handleAppStateChange = (nextAppState) => { + if (nextAppState !== 'active') { + this.changeDraft(this.state.value); + } + }; + handleEndEditing = (e) => { if (e && e.nativeEvent) { this.changeDraft(e.nativeEvent.text || ''); diff --git a/app/components/safe_area_view/safe_area_view.ios.js b/app/components/safe_area_view/safe_area_view.ios.js index 4758c992e..00d73702a 100644 --- a/app/components/safe_area_view/safe_area_view.ios.js +++ b/app/components/safe_area_view/safe_area_view.ios.js @@ -6,6 +6,8 @@ import PropTypes from 'prop-types'; import {Dimensions, Keyboard, NativeModules, View} from 'react-native'; import SafeArea from 'react-native-safe-area'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; + import {DeviceTypes} from 'app/constants'; import mattermostManaged from 'app/mattermost_managed'; @@ -52,6 +54,7 @@ export default class SafeAreaIos extends PureComponent { componentDidMount() { Dimensions.addEventListener('change', this.getSafeAreaInsets); + EventEmitter.on('update_safe_area_view', this.getSafeAreaInsets); this.keyboardDidShowListener = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow); this.keyboardDidHideListener = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide); this.getStatusBarHeight(); @@ -60,6 +63,7 @@ export default class SafeAreaIos extends PureComponent { componentWillUnmount() { this.mounted = false; Dimensions.removeEventListener('change', this.getSafeAreaInsets); + EventEmitter.off('update_safe_area_view', this.getSafeAreaInsets); this.keyboardDidShowListener.remove(); this.keyboardDidHideListener.remove(); this.mounted = false; diff --git a/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap b/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap index e69b5ce57..1bfdb0263 100644 --- a/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap +++ b/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap @@ -80,14 +80,12 @@ exports[`ChannelItem should match snapshot 1`] = ` style={ Array [ Object { + "alignSelf": "center", "color": "rgba(255,255,255,0.4)", "flex": 1, "fontSize": 14, "fontWeight": "600", - "height": "100%", - "lineHeight": 44, "paddingRight": 10, - "textAlignVertical": "center", }, Object { "color": "#ffffff", @@ -194,14 +192,12 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you style={ Array [ Object { + "alignSelf": "center", "color": "rgba(255,255,255,0.4)", "flex": 1, "fontSize": 14, "fontWeight": "600", - "height": "100%", - "lineHeight": 44, "paddingRight": 10, - "textAlignVertical": "center", }, Object { "color": "#ffffff", @@ -308,14 +304,12 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you style={ Array [ Object { + "alignSelf": "center", "color": "rgba(255,255,255,0.4)", "flex": 1, "fontSize": 14, "fontWeight": "600", - "height": "100%", - "lineHeight": 44, "paddingRight": 10, - "textAlignVertical": "center", }, Object { "color": "#ffffff", @@ -422,14 +416,12 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh style={ Array [ Object { + "alignSelf": "center", "color": "rgba(255,255,255,0.4)", "flex": 1, "fontSize": 14, "fontWeight": "600", - "height": "100%", - "lineHeight": 44, "paddingRight": 10, - "textAlignVertical": "center", }, Object { "color": "#ffffff", @@ -525,14 +517,12 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes style={ Array [ Object { + "alignSelf": "center", "color": "rgba(255,255,255,0.4)", "flex": 1, "fontSize": 14, "fontWeight": "600", - "height": "100%", - "lineHeight": 44, "paddingRight": 10, - "textAlignVertical": "center", }, Object { "color": "#ffffff", @@ -634,14 +624,12 @@ exports[`ChannelItem should match snapshot with draft 1`] = ` style={ Array [ Object { + "alignSelf": "center", "color": "rgba(255,255,255,0.4)", "flex": 1, "fontSize": 14, "fontWeight": "600", - "height": "100%", - "lineHeight": 44, "paddingRight": 10, - "textAlignVertical": "center", }, Object { "color": "#ffffff", @@ -739,14 +727,12 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = ` style={ Array [ Object { + "alignSelf": "center", "color": "rgba(255,255,255,0.4)", "flex": 1, "fontSize": 14, "fontWeight": "600", - "height": "100%", - "lineHeight": 44, "paddingRight": 10, - "textAlignVertical": "center", }, Object { "color": "#ffffff", diff --git a/app/components/sidebars/main/channels_list/channel_item/channel_item.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.js index a5d01ffce..e5af091d4 100644 --- a/app/components/sidebars/main/channels_list/channel_item/channel_item.js +++ b/app/components/sidebars/main/channels_list/channel_item/channel_item.js @@ -240,10 +240,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { fontSize: 14, fontWeight: '600', paddingRight: 10, - height: '100%', flex: 1, - textAlignVertical: 'center', - lineHeight: 44, + alignSelf: 'center', }, textActive: { color: theme.sidebarTextActiveColor, diff --git a/app/components/sidebars/main/main_sidebar.js b/app/components/sidebars/main/main_sidebar.js index 1b048cc00..deb649a42 100644 --- a/app/components/sidebars/main/main_sidebar.js +++ b/app/components/sidebars/main/main_sidebar.js @@ -5,6 +5,7 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import { BackHandler, + Dimensions, Keyboard, StyleSheet, View, @@ -17,6 +18,7 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter'; import SafeAreaView from 'app/components/safe_area_view'; import DrawerLayout, {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout'; import {DeviceTypes} from 'app/constants'; +import mattermostManaged from 'app/mattermost_managed'; import tracker from 'app/utils/time_tracker'; import {t} from 'app/utils/i18n'; @@ -69,15 +71,19 @@ export default class ChannelSidebar extends Component { openDrawerOffset, drawerOpened: false, searching: false, + isSplitView: false, }; } componentDidMount() { + this.mounted = true; this.props.actions.getTeams(); + this.handleDimensions(); EventEmitter.on('close_channel_drawer', this.closeChannelDrawer); EventEmitter.on('renderDrawer', this.handleShowDrawerContent); EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle); BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack); + Dimensions.addEventListener('change', this.handleDimensions); } componentWillReceiveProps(nextProps) { @@ -97,7 +103,7 @@ export default class ChannelSidebar extends Component { shouldComponentUpdate(nextProps, nextState) { const {currentTeamId, deviceWidth, isLandscape, teamsCount} = this.props; - const {openDrawerOffset, show, searching} = this.state; + const {openDrawerOffset, isSplitView, show, searching} = this.state; if (nextState.openDrawerOffset !== openDrawerOffset || nextState.show !== show || nextState.searching !== searching) { return true; @@ -105,14 +111,17 @@ export default class ChannelSidebar extends Component { return nextProps.currentTeamId !== currentTeamId || nextProps.isLandscape !== isLandscape || nextProps.deviceWidth !== deviceWidth || - nextProps.teamsCount !== teamsCount; + nextProps.teamsCount !== teamsCount || + nextState.isSplitView !== isSplitView; } componentWillUnmount() { + this.mounted = false; EventEmitter.off('close_channel_drawer', this.closeChannelDrawer); EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle); EventEmitter.off('renderDrawer', this.handleShowDrawerContent); BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack); + Dimensions.addEventListener('change', this.handleDimensions); } handleAndroidBack = () => { @@ -124,6 +133,15 @@ export default class ChannelSidebar extends Component { return false; }; + handleDimensions = () => { + if (DeviceTypes.IS_TABLET && this.mounted) { + mattermostManaged.isRunningInSplitView().then((result) => { + const isSplitView = Boolean(result.isSplitView); + this.setState({isSplitView}); + }); + } + }; + handleShowDrawerContent = () => { this.setState({show: true}); }; @@ -378,6 +396,7 @@ export default class ChannelSidebar extends Component { render() { const {children, deviceWidth} = this.props; const {openDrawerOffset} = this.state; + const isTablet = DeviceTypes.IS_TABLET && !this.state.isSplitView; const drawerWidth = DeviceTypes.IS_TABLET ? TABLET_WIDTH : (deviceWidth - openDrawerOffset); return ( @@ -388,7 +407,7 @@ export default class ChannelSidebar extends Component { onDrawerOpen={this.handleDrawerOpen} drawerWidth={drawerWidth} useNativeAnimations={true} - isTablet={DeviceTypes.IS_TABLET} + isTablet={isTablet} > {children} diff --git a/app/init/credentials.js b/app/init/credentials.js new file mode 100644 index 000000000..14b401467 --- /dev/null +++ b/app/init/credentials.js @@ -0,0 +1,128 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import AsyncStorage from '@react-native-community/async-storage'; +import * as KeyChain from 'react-native-keychain'; + +import {Client4} from 'mattermost-redux/client'; + +import mattermostManaged from 'app/mattermost_managed'; +import EphemeralStore from 'app/store/ephemeral_store'; +import {setCSRFFromCookie} from 'app/utils/security'; + +const CURRENT_SERVER = '@currentServerUrl'; + +export const getCurrentServerUrl = async () => { + return AsyncStorage.getItem(CURRENT_SERVER); +}; + +export const setAppCredentials = (deviceToken, currentUserId, token, url) => { + if (!currentUserId) { + return; + } + + // Only save to keychain if the url and token are set + if (url && token) { + try { + const username = `${deviceToken}, ${currentUserId}`; + + EphemeralStore.deviceToken = deviceToken; + AsyncStorage.setItem(CURRENT_SERVER, url); + KeyChain.setInternetCredentials(url, username, token, {accessGroup: mattermostManaged.appGroupIdentifier}); + } catch (e) { + console.warn('could not set credentials', e); //eslint-disable-line no-console + } + } +}; + +export const getAppCredentials = async () => { + const serverUrl = await AsyncStorage.getItem(CURRENT_SERVER); + + if (serverUrl) { + return getInternetCredentials(serverUrl); + } + + return getCredentialsFromGenericKeyChain(); +}; + +export const removeAppCredentials = async () => { + const url = await getCurrentServerUrl(); + + Client4.setCSRF(null); + Client4.serverVersion = ''; + Client4.setUserId(''); + Client4.setToken(''); + Client4.setUrl(''); + + if (url) { + KeyChain.resetInternetCredentials(url); + } + + KeyChain.resetGenericPassword(); + AsyncStorage.removeItem(CURRENT_SERVER); +}; + +async function getCredentialsFromGenericKeyChain() { + try { + const credentials = await KeyChain.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; + + // if for any case the url and the token aren't valid proceed with re-hydration + if (url && url !== 'undefined' && token && token !== 'undefined') { + Client4.setUserId(currentUserId); + Client4.setUrl(url); + Client4.setToken(token); + await setCSRFFromCookie(url); + + // Migration: remove the generic credentials and add a server specific one + setAppCredentials(deviceToken, currentUserId, token, url); + KeyChain.resetGenericPassword(); + + return { + username: usernameParsed, + password: token, + }; + } + } + } + + return null; + } catch (e) { + return null; + } +} + +async function getInternetCredentials(url) { + try { + const credentials = await KeyChain.getInternetCredentials(url); + + if (credentials) { + const usernameParsed = credentials.username.split(','); + const token = credentials.password; + const [deviceToken, currentUserId] = usernameParsed; + + if (token && token !== 'undefined') { + EphemeralStore.deviceToken = deviceToken; + Client4.setUserId(currentUserId); + Client4.setUrl(url); + Client4.setToken(token); + await setCSRFFromCookie(url); + + return credentials; + } + } + + return null; + } catch (e) { + return null; + } +} diff --git a/app/init/emm_provider.js b/app/init/emm_provider.js new file mode 100644 index 000000000..77dfad013 --- /dev/null +++ b/app/init/emm_provider.js @@ -0,0 +1,165 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Alert, Platform} from 'react-native'; + +import {handleLoginIdChanged} from 'app/actions/views/login'; +import {handleServerUrlChanged} from 'app/actions/views/select_server'; +import {getTranslations} from 'app/i18n'; +import mattermostBucket from 'app/mattermost_bucket'; +import mattermostManaged from 'app/mattermost_managed'; +import {getCurrentLocale} from 'app/selectors/i18n'; +import {t} from 'app/utils/i18n'; + +import {getAppCredentials} from './credentials'; + +import LocalConfig from 'assets/config'; + +class EMMProvider { + constructor() { + this.enabled = false; + this.performingAuthentication = false; + this.previousAppState = null; + + this.inAppPinCode = false; + this.blurApplicationScreen = false; + this.jailbreakProtection = false; + this.vendor = null; + + this.allowOtherServers = true; + this.emmServerUrl = null; + this.emmUsername = null; + } + + checkIfDeviceIsTrusted = (store) => { + const isTrusted = mattermostManaged.isTrustedDevice(); + + if (!isTrusted) { + const state = store.getState(); + const locale = getCurrentLocale(state); + const translations = getTranslations(locale); + Alert.alert( + translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor), + translations[t('mobile.managed.jailbreak')].replace('{vendor}', this.vendor), + [{ + text: translations[t('mobile.managed.exit')], + style: 'destructive', + onPress: () => { + mattermostManaged.quitApp(); + }, + }], + {cancelable: false} + ); + } + }; + + handleAuthentication = async (store, prompt = true) => { + this.performingAuthentication = true; + const isSecured = await mattermostManaged.isDeviceSecure(); + const state = store.getState(); + const locale = getCurrentLocale(state); + const translations = getTranslations(locale); + + if (isSecured) { + try { + mattermostBucket.setPreference('emm', this.vendor); + if (prompt) { + await mattermostManaged.authenticate({ + reason: translations[t('mobile.managed.secured_by')].replace('{vendor}', this.vendor), + fallbackToPasscode: true, + suppressEnterPassword: true, + }); + } + } catch (err) { + mattermostManaged.quitApp(); + return false; + } + } else { + await this.showNotSecuredAlert(translations); + + mattermostManaged.quitApp(); + return false; + } + + this.setPerformingAuthentication(false); + return true; + }; + + handleManagedConfig = async (store) => { + if (this.performingAuthentication) { + return true; + } + + const {dispatch} = store; + + if (LocalConfig.AutoSelectServerUrl) { + dispatch(handleServerUrlChanged(LocalConfig.DefaultServerUrl)); + this.allowOtherServers = false; + } + + const managedConfig = await mattermostManaged.getConfig(); + + if (managedConfig && Object.keys(managedConfig).length) { + this.enabled = true; + this.inAppPinCode = managedConfig.inAppPinCode === 'true'; + + this.blurApplicationScreen = managedConfig.blurApplicationScreen === 'true'; + this.jailbreakProtection = managedConfig.jailbreakProtection === 'true'; + this.vendor = managedConfig.vendor || 'Mattermost'; + + const credentials = await getAppCredentials(); + if (!credentials) { + this.emmServerUrl = managedConfig.serverUrl; + this.emmUsername = managedConfig.username; + + if (managedConfig.allowOtherServers && managedConfig.allowOtherServers === 'false') { + this.allowOtherServers = false; + } + } + + if (this.blurApplicationScreen) { + mattermostManaged.blurAppScreen(true); + } + + if (this.emmServerUrl) { + dispatch(handleServerUrlChanged(this.emmServerUrl)); + } + + if (this.emmUsername) { + dispatch(handleLoginIdChanged(this.emmUsername)); + } + } + + return true; + }; + + showNotSecuredAlert = (translations) => { + return new Promise((resolve) => { + const options = []; + + if (Platform.OS === 'android') { + options.push({ + text: translations[t('mobile.managed.settings')], + onPress: () => { + mattermostManaged.goToSecuritySettings(); + }, + }); + } + + options.push({ + text: translations[t('mobile.managed.exit')], + onPress: resolve, + style: 'cancel', + }); + + Alert.alert( + translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor), + Platform.OS === 'ios' ? translations[t('mobile.managed.not_secured.ios')] : translations[t('mobile.managed.not_secured.android')], + options, + {cancelable: false, onDismiss: resolve}, + ); + }); + } +} + +export default new EMMProvider(); diff --git a/app/fetch_preconfig.js b/app/init/fetch.js similarity index 97% rename from app/fetch_preconfig.js rename to app/init/fetch.js index f5f74050f..57cd167a4 100644 --- a/app/fetch_preconfig.js +++ b/app/init/fetch.js @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import {Platform} from 'react-native'; +import DeviceInfo from 'react-native-device-info'; import RNFetchBlob from 'rn-fetch-blob'; import urlParse from 'url-parse'; @@ -146,6 +147,8 @@ const initFetchConfig = async () => { // no managed config } + Client4.setUserAgent(DeviceInfo.getUserAgent()); + if (Platform.OS === 'ios') { const certificate = await mattermostBucket.getPreference('cert'); fetchConfig = { diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js new file mode 100644 index 000000000..df17fb796 --- /dev/null +++ b/app/init/global_event_handler.js @@ -0,0 +1,232 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Alert, AppState, Dimensions, Linking, NativeModules, Platform} from 'react-native'; +import DeviceInfo from 'react-native-device-info'; +import semver from 'semver'; + +import {setAppState, setServerVersion} from 'mattermost-redux/actions/general'; +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 EventEmitter from 'mattermost-redux/utils/event_emitter'; + +import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device'; +import {selectDefaultChannel} from 'app/actions/views/channel'; +import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from 'app/actions/views/root'; +import {NavigationTypes} from 'app/constants'; +import {getTranslations} from 'app/i18n'; +import mattermostManaged from 'app/mattermost_managed'; +import PushNotifications from 'app/push_notifications'; +import {getCurrentLocale} from 'app/selectors/i18n'; +import {t} from 'app/utils/i18n'; +import {deleteFileCache} from 'app/utils/file'; + +import LocalConfig from 'assets/config'; + +import {getAppCredentials, removeAppCredentials} from './credentials'; +import emmProvider from './emm_provider'; + +const {StatusBarManager} = NativeModules; +const PROMPT_IN_APP_PIN_CODE_AFTER = 5 * 1000; + +class GlobalEventHandler { + constructor() { + EventEmitter.on(NavigationTypes.NAVIGATION_RESET, this.onLogout); + EventEmitter.on(NavigationTypes.RESTART_APP, this.onRestartApp); + EventEmitter.on(General.SERVER_VERSION_CHANGED, this.onServerVersionChanged); + EventEmitter.on(General.CONFIG_CHANGED, this.onServerConfigChanged); + EventEmitter.on(General.SWITCH_TO_DEFAULT_CHANNEL, this.onSwitchToDefaultChannel); + Dimensions.addEventListener('change', this.onOrientationChange); + AppState.addEventListener('change', this.onAppStateChange); + Linking.addEventListener('url', this.onDeepLink); + } + + appActive = async () => { + // if the app is being controlled by an EMM provider + if (emmProvider.enabled && emmProvider.inAppPinCode) { + const authExpired = (Date.now() - emmProvider.inBackgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER; + + // Once the app becomes active we check if the device needs to have a passcode set + const prompt = emmProvider.inBackgroundSince && authExpired; // if more than 5 minutes have passed prompt for passcode + await emmProvider.handleAuthentication(this.store, prompt); + } + + emmProvider.inBackgroundSince = null; + }; + + appInactive = () => { + const {dispatch} = this.store; + + // 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 + emmProvider.inBackgroundSince = Date.now(); + + dispatch(startDataCleanup()); + }; + + configure = (opts) => { + this.store = opts.store; + this.launchApp = opts.launchApp; + + const window = Dimensions.get('window'); + this.onOrientationChange({window}); + + this.StatusBarSizeIOS = require('react-native-status-bar-size'); + if (Platform.OS === 'ios') { + this.StatusBarSizeIOS.addEventListener('willChange', this.onStatusBarHeightChange); + + StatusBarManager.getHeight( + (data) => { + this.onStatusBarHeightChange(data.height); + } + ); + } + + this.JavascriptAndNativeErrorHandler = require('app/utils/error_handling').default; + this.JavascriptAndNativeErrorHandler.initializeErrorHandling(this.store); + + mattermostManaged.addEventListener('managedConfigDidChange', this.onManagedConfigurationChange); + }; + + configureAnalytics = (config) => { + const initAnalytics = require('app/utils/segment').init; + + if (!__DEV__ && config && config.DiagnosticsEnabled === 'true' && config.DiagnosticId && LocalConfig.SegmentApiKey) { + initAnalytics(config); + } else { + global.analytics = null; + } + }; + + onAppStateChange = (appState) => { + const isActive = appState === 'active'; + const isBackground = appState === 'background'; + + if (this.store) { + this.store.dispatch(setAppState(isActive)); + } + + if (isActive && emmProvider.previousAppState === 'background') { + this.appActive(); + } else if (isBackground) { + this.appInactive(); + } + + emmProvider.previousAppState = appState; + }; + + onDeepLink = (event) => { + const {url} = event; + this.store.dispatch(setDeepLinkURL(url)); + }; + + onManagedConfigurationChange = () => { + emmProvider.handleManagedConfig(this.store, true); + }; + + onServerConfigChanged = (config) => { + this.configureAnalytics(config); + }; + + onLogout = async () => { + this.store.dispatch(closeWebSocket(false)); + this.store.dispatch(setServerVersion('')); + deleteFileCache(); + removeAppCredentials(); + + PushNotifications.setApplicationIconBadgeNumber(0); + PushNotifications.cancelAllLocalNotifications(); // TODO: Only cancel the notification that belongs to this server + + if (this.launchApp) { + this.launchApp(); + } + }; + + onOrientationChange = (dimensions) => { + if (this.store) { + const {dispatch} = this.store; + if (DeviceInfo.isTablet()) { + dispatch(setDeviceAsTablet()); + } + + const {height, width} = dimensions.window; + const orientation = height > width ? 'PORTRAIT' : 'LANDSCAPE'; + + dispatch(setDeviceOrientation(orientation)); + dispatch(setDeviceDimensions(height, width)); + } + }; + + onRestartApp = async () => { + await this.store.dispatch(loadConfigAndLicense()); + await this.store.dispatch(loadMe()); + + const window = Dimensions.get('window'); + this.onOrientationChange({window}); + + if (Platform.OS === 'ios') { + StatusBarManager.getHeight( + (data) => { + this.onStatusBarHeightChange(data.height); + } + ); + } + + if (this.launchApp) { + this.launchApp(); + } + }; + + onServerVersionChanged = async (serverVersion) => { + const {dispatch, getState} = this.store; + const state = getState(); + const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0]; + const locale = getCurrentLocale(state); + const translations = getTranslations(locale); + + if (serverVersion) { + if (semver.valid(version) && semver.lt(version, LocalConfig.MinServerVersion)) { + Alert.alert( + translations[t('mobile.server_upgrade.title')], + translations[t('mobile.server_upgrade.description')], + [{ + text: translations[t('mobile.server_upgrade.button')], + onPress: this.serverUpgradeNeeded, + }], + {cancelable: false} + ); + } else if (state.entities.users && state.entities.users.currentUserId) { + dispatch(setServerVersion(serverVersion)); + const data = await dispatch(loadConfigAndLicense()); + this.configureAnalytics(data.config); + } + } + }; + + onStatusBarHeightChange = (nextStatusBarHeight) => { + this.store.dispatch(setStatusBarHeight(nextStatusBarHeight)); + }; + + onSwitchToDefaultChannel = (teamId) => { + this.store.dispatch(selectDefaultChannel(teamId)); + }; + + serverUpgradeNeeded = async () => { + const {dispatch} = this.store; + + dispatch(setServerVersion('')); + Client4.serverVersion = ''; + PushNotifications.setApplicationIconBadgeNumber(0); + PushNotifications.cancelAllLocalNotifications(); // TODO: Only cancel the notification that belongs to this server + + const credentials = await getAppCredentials(); + + if (credentials) { + dispatch(logout()); + } + }; +} + +export default new GlobalEventHandler(); diff --git a/app/mattermost.js b/app/mattermost.js index 32744fff4..8f2d56903 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -1,505 +1,97 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -/* eslint-disable global-require*/ -import { - Alert, - AppState, - Dimensions, - InteractionManager, - NativeModules, - Platform, - YellowBox, -} from 'react-native'; -const {StatusBarManager, MattermostShare, Initialization} = NativeModules; - -import DeviceInfo from 'react-native-device-info'; -import {Navigation, NativeEventsReceiver} from 'react-native-navigation'; +import {Linking, NativeModules, Platform} from 'react-native'; +import {Navigation} from 'react-native-navigation'; import {Provider} from 'react-redux'; -import semver from 'semver'; -import {Client4} from 'mattermost-redux/client'; -import {setAppState, setServerVersion} from 'mattermost-redux/actions/general'; -import {loadMe, logout} from 'mattermost-redux/actions/users'; -import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; -import {General} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {loadMe} from 'mattermost-redux/actions/users'; import {resetToChannel, resetToSelectServer} from 'app/actions/navigation'; -import {selectDefaultChannel} from 'app/actions/views/channel'; -import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device'; -import {handleLoginIdChanged} from 'app/actions/views/login'; -import {handleServerUrlChanged} from 'app/actions/views/select_server'; -import {loadConfigAndLicense, startDataCleanup} from 'app/actions/views/root'; - +import {setDeepLinkURL} from 'app/actions/views/root'; 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 {getAppCredentials} from 'app/init/credentials'; +import emmProvider from 'app/init/emm_provider'; +import 'app/init/fetch'; +import globalEventHandler from 'app/init/global_event_handler'; import {registerScreens} from 'app/screens'; -import {deleteFileCache} from 'app/utils/file'; -import avoidNativeBridge from 'app/utils/avoid_native_bridge'; -import {t} from 'app/utils/i18n'; -import LocalConfig from 'assets/config'; -import telemetry from 'app/telemetry'; - -import App from './app'; +import configureStore from 'app/store'; import EphemeralStore from 'app/store/ephemeral_store'; -import './fetch_preconfig'; +import telemetry from 'app/telemetry'; +import pushNotificationsUtils from 'app/utils/push_notifications'; -const PROMPT_IN_APP_PIN_CODE_AFTER = 5 * 60 * 1000; - -// Hide warnings caused by React Native (https://github.com/facebook/react-native/issues/20841) -YellowBox.ignoreWarnings(['Require cycle: node_modules/react-native/Libraries/Network/fetch.js']); - -export const app = new App(); +const {MattermostShare} = NativeModules; +const sharedExtensionStarted = Platform.OS === 'android' && MattermostShare.isOpened; export const store = configureStore(initialState); -registerScreens(store, Provider); -const lazyLoadExternalModules = () => { - const StatusBarSizeIOS = require('react-native-status-bar-size'); - const initializeErrorHandling = require('app/utils/error_handling').initializeErrorHandling; - return { - StatusBarSizeIOS, - initializeErrorHandling, - }; -}; +const init = async () => { + if (EphemeralStore.appStarted) { + launchAppAndAuthenticateIfNeeded(); + return; + } -const lazyLoadAnalytics = () => { - const initAnalytics = require('app/utils/segment').init; - - return { - initAnalytics, - }; -}; - -export const initializeModules = () => { - const { - StatusBarSizeIOS, - initializeErrorHandling, - } = lazyLoadExternalModules(); - const { - config, - } = store.getState().entities.general; - const window = Dimensions.get('window'); - - 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.SWITCH_TO_DEFAULT_CHANNEL, handleSwitchToDefaultChannel); - Dimensions.addEventListener('change', handleOrientationChange); - mattermostManaged.addEventListener('managedConfigDidChange', () => { - handleManagedConfig(true); + pushNotificationsUtils.configure(store); + globalEventHandler.configure({ + store, + launchApp, }); - if (config) { - configureAnalytics(config); + registerScreens(store, Provider); + + if (sharedExtensionStarted) { + EphemeralStore.appStarted = true; } - handleOrientationChange({window}); - - if (Platform.OS === 'ios') { - StatusBarSizeIOS.addEventListener('willChange', handleStatusBarHeightChange); - - StatusBarManager.getHeight( - (data) => { - handleStatusBarHeightChange(data.height); - } - ); + if (!EphemeralStore.appStarted) { + launchAppAndAuthenticateIfNeeded(); } }; -const configureAnalytics = (config) => { - const { - initAnalytics, - } = lazyLoadAnalytics(); - if (!__DEV__ && config && config.DiagnosticsEnabled === 'true' && config.DiagnosticId && LocalConfig.SegmentApiKey) { - initAnalytics(config); +const launchApp = async () => { + telemetry.start([ + 'start:select_server_screen', + 'start:channel_screen', + ]); + + const credentials = await getAppCredentials(); + if (credentials) { + store.dispatch(loadMe()); + store.dispatch(resetToChannel({skipMetrics: true})); } else { - global.analytics = null; - } -}; - -const resetBadgeAndVersion = () => { - Client4.serverVersion = ''; - Client4.setUserId(''); - PushNotifications.setApplicationIconBadgeNumber(0); - PushNotifications.cancelAllLocalNotifications(); - store.dispatch(setServerVersion('')); -}; - -const handleLogout = () => { - Client4.setCSRF(null); - store.dispatch(closeWebSocket(false)); - - app.setAppStarted(true); - app.clearNativeCache(); - deleteFileCache(); - resetBadgeAndVersion(); - store.dispatch(resetToSelectServer(app.allowOtherServers)); -}; - -const restartApp = async () => { - Navigation.dismissModal({animationType: 'none'}); - - try { - const window = Dimensions.get('window'); - - handleOrientationChange({window}); - await store.dispatch(loadConfigAndLicense()); - await store.dispatch(loadMe()); - - if (Platform.OS === 'ios') { - StatusBarManager.getHeight( - (data) => { - handleStatusBarHeightChange(data.height); - } - ); - } - } catch (e) { - console.warn('Failed to load initial data while restarting', e); // eslint-disable-line no-console + store.dispatch(resetToSelectServer(emmProvider.allowOtherServers)); } - store.dispatch(resetToChannel()); + telemetry.startSinceLaunch(['start:splash_screen']); + EphemeralStore.appStarted = true; }; -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(); +const launchAppAndAuthenticateIfNeeded = async () => { + await emmProvider.handleManagedConfig(store); + await launchApp(); - if (serverVersion) { - if (semver.valid(version) && semver.lt(version, LocalConfig.MinServerVersion)) { - Alert.alert( - translations[t('mobile.server_upgrade.title')], - translations[t('mobile.server_upgrade.description')], - [{ - text: translations[t('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); - } - } -}; - -const handleConfigChanged = (config) => { - configureAnalytics(config); -}; - -const handleServerVersionUpgradeNeeded = async () => { - const {dispatch, getState} = store; - - resetBadgeAndVersion(); - - if (getState().entities.general.credentials.token) { - InteractionManager.runAfterInteractions(() => { - dispatch(logout()); - }); - } -}; - -const handleStatusBarHeightChange = (nextStatusBarHeight) => { - store.dispatch(setStatusBarHeight(nextStatusBarHeight)); -}; - -const handleOrientationChange = (dimensions) => { - const {dispatch} = store; - if (DeviceInfo.isTablet()) { - dispatch(setDeviceAsTablet()); - } - - const {height, width} = dimensions.window; - const orientation = height > width ? 'PORTRAIT' : 'LANDSCAPE'; - - dispatch(setDeviceOrientation(orientation)); - dispatch(setDeviceDimensions(height, width)); -}; - -export const handleManagedConfig = async (eventFromEmmServer = false) => { - if (app.performingEMMAuthentication) { - return true; - } - - const {dispatch, getState} = store; - const state = getState(); - - 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 === 'true'; - blurApplicationScreen = config.blurApplicationScreen === 'true'; - 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[t('mobile.managed.blocked_by')].replace('{vendor}', vendor), - translations[t('mobile.managed.jailbreak')].replace('{vendor}', vendor), - [{ - text: translations[t('mobile.managed.exit')], - style: 'destructive', - onPress: () => { - mattermostManaged.quitApp(); - }, - }], - {cancelable: false} - ); - return false; - } - } - - if (authNeeded && !eventFromEmmServer) { - 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, prompt = true) => { - app.setPerformingEMMAuthentication(true); - const isSecured = await mattermostManaged.isDeviceSecure(); - const translations = app.getTranslations(); - - if (isSecured) { - try { - mattermostBucket.setPreference('emm', vendor); - if (prompt) { - await mattermostManaged.authenticate({ - reason: translations[t('mobile.managed.secured_by')].replace('{vendor}', vendor), - fallbackToPasscode: true, - suppressEnterPassword: true, - }); - } - } catch (err) { - mattermostManaged.quitApp(); - return false; - } - } else { - await showNotSecuredAlert(vendor, translations); - - mattermostManaged.quitApp(); - return false; - } - - app.setPerformingEMMAuthentication(false); - return true; -}; - -function showNotSecuredAlert(vendor, translations) { - return new Promise((resolve) => { - const options = []; - - if (Platform.OS === 'android') { - options.push({ - text: translations[t('mobile.managed.settings')], - onPress: () => { - mattermostManaged.goToSecuritySettings(); - }, - }); + if (emmProvider.enabled) { + if (emmProvider.jailbreakProtection) { + emmProvider.checkIfDeviceIsTrusted(); } - options.push({ - text: translations[t('mobile.managed.exit')], - onPress: resolve, - style: 'cancel', - }); - - Alert.alert( - translations[t('mobile.managed.blocked_by')].replace('{vendor}', vendor), - Platform.OS === 'ios' ? translations[t('mobile.managed.not_secured.ios')] : translations[t('mobile.managed.not_secured.android')], - options, - {cancelable: false, onDismiss: resolve}, - ); - }); -} - -const handleSwitchToDefaultChannel = (teamId) => { - store.dispatch(selectDefaultChannel(teamId)); -}; - -const handleAppStateChange = (appState) => { - const isActive = appState === 'active'; - const isBackground = appState === 'background'; - - store.dispatch(setAppState(isActive)); - - if (isActive && app.previousAppState === 'background') { - handleAppActive(); - } else if (isBackground) { - handleAppInActive(); - } - - app.previousAppState = appState; -}; - -const handleAppActive = async () => { - // 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); - } - - // if the app is being controlled by an EMM provider - if (app.emmEnabled) { - const config = await mattermostManaged.getConfig(); - const authNeeded = config.inAppPinCode === 'true'; - const authExpired = (Date.now() - app.inBackgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER; - - // Once the app becomes active we check if the device needs to have a passcode set - if (authNeeded) { - const prompt = app.inBackgroundSince && authExpired; // if more than 5 minutes have passed prompt for passcode - await handleAuthentication(config.vendor, prompt); + if (emmProvider.inAppPinCode) { + await emmProvider.handleAuthentication(store); } } - app.setInBackgroundSince(null); -}; - -const handleAppInActive = () => { - const {dispatch, getState} = store; - const theme = getTheme(getState()); - - // 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()); -}; - -AppState.addEventListener('change', handleAppStateChange); - -const launchEntry = () => { - Navigation.events().registerAppLaunchedListener(() => { - telemetry.start([ - 'start:select_server_screen', - 'start:channel_screen', - ]); - - // Keep track of the latest componentId to appear and disappear - Navigation.events().registerComponentDidAppearListener(({componentId}) => { - EphemeralStore.addComponentIdToStack(componentId); - }); - Navigation.events().registerComponentDidDisappearListener(({componentId}) => { - EphemeralStore.removeComponentIdFromStack(componentId); - }); - - Navigation.setRoot({ - root: { - stack: { - children: [{ - component: { - name: 'Entry', - }, - }], - options: { - layout: { - backgroundColor: 'transparent', - }, - statusBar: { - visible: true, - }, - topBar: { - visible: false, - height: 0, - }, - }, - }, - }, - }); - - telemetry.startSinceLaunch(['start:splash_screen']); + Linking.getInitialURL().then((url) => { + store.dispatch(setDeepLinkURL(url)); }); }; -configurePushNotifications(); -const startedSharedExtension = Platform.OS === 'android' && MattermostShare.isOpened; -const fromPushNotification = Platform.OS === 'android' && Initialization.replyFromPushNotification; +Navigation.events().registerAppLaunchedListener(() => { + init(); -if (startedSharedExtension || fromPushNotification) { - // Hold on launching Entry screen - app.canLaunchEntry(false); - - // Listen for when the user opens the app - new NativeEventsReceiver().appLaunched(() => { - app.canLaunchEntry(true); - launchEntry(); + // Keep track of the latest componentId to appear and disappear + Navigation.events().registerComponentDidAppearListener(({componentId}) => { + EphemeralStore.addComponentIdToStack(componentId); }); -} - -if (app.canLaunchEntry) { - launchEntry(); -} + Navigation.events().registerComponentDidDisappearListener(({componentId}) => { + EphemeralStore.removeComponentIdFromStack(componentId); + }); +}); diff --git a/app/mattermost_managed/mattermost-managed.android.js b/app/mattermost_managed/mattermost-managed.android.js index da192cb58..778396a9f 100644 --- a/app/mattermost_managed/mattermost-managed.android.js +++ b/app/mattermost_managed/mattermost-managed.android.js @@ -36,6 +36,9 @@ export default { }, authenticate: LocalAuth.auth, blurAppScreen: MattermostManaged.blurAppScreen, + appGroupIdentifier: null, + hasSafeAreaInsets: null, + isRunningInSplitView: MattermostManaged.isRunningInSplitView, getConfig: async () => { try { cachedConfig = await MattermostManaged.getConfig(); diff --git a/app/mattermost_managed/mattermost-managed.ios.js b/app/mattermost_managed/mattermost-managed.ios.js index 7b6d81525..8073f9a26 100644 --- a/app/mattermost_managed/mattermost-managed.ios.js +++ b/app/mattermost_managed/mattermost-managed.ios.js @@ -51,7 +51,9 @@ export default { getCachedConfig: () => { return cachedConfig; }, + appGroupIdentifier: MattermostManaged.appGroupIdentifier, hasSafeAreaInsets: MattermostManaged.hasSafeAreaInsets, + isRunningInSplitView: MattermostManaged.isRunningInSplitView, isDeviceSecure: async () => { try { return await LocalAuth.isDeviceSecure(); diff --git a/app/push_notifications/push_notifications.android.js b/app/push_notifications/push_notifications.android.js index 9e30212f1..2e3943a32 100644 --- a/app/push_notifications/push_notifications.android.js +++ b/app/push_notifications/push_notifications.android.js @@ -1,11 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {AppRegistry, AppState, NativeModules} from 'react-native'; +import {AppState, NativeModules} from 'react-native'; import {NotificationsAndroid, PendingNotifications} from 'react-native-notifications'; -import Notification from 'react-native-notifications/notification.android'; - -import {emptyFunction} from 'app/utils/general'; const {NotificationPreferences} = NativeModules; @@ -37,23 +34,6 @@ class PushNotification { this.handleNotification(data, true); } }); - - AppRegistry.registerHeadlessTask('notificationReplied', () => async (deviceNotification) => { - const notification = new Notification(deviceNotification); - const data = notification.getData(); - const completed = emptyFunction; - - if (this.onReply) { - this.onReply(data, data.text, parseInt(data.badge, 10) - parseInt(data.msg_count, 10), completed); - } else { - this.deviceNotification = { - data, - text: data.text, - badge: parseInt(data.badge, 10) - parseInt(data.msg_count, 10), - completed, // used to identify that the notification belongs to a reply - }; - } - }); } handleNotification = (data, userInteraction) => { diff --git a/app/reducers/device/index.js b/app/reducers/device/index.js index 14a8e351d..5145f3e85 100644 --- a/app/reducers/device/index.js +++ b/app/reducers/device/index.js @@ -8,7 +8,6 @@ import dimension from './dimension'; import isTablet from './is_tablet'; import orientation from './orientation'; import statusBarHeight from './status_bar'; -import websocket from './websocket'; export default combineReducers({ connection, @@ -16,5 +15,4 @@ export default combineReducers({ isTablet, orientation, statusBarHeight, - websocket, }); diff --git a/app/reducers/device/websocket.js b/app/reducers/device/websocket.js deleted file mode 100644 index 803e8fdde..000000000 --- a/app/reducers/device/websocket.js +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {GeneralTypes, UserTypes} from 'mattermost-redux/action_types'; - -function getInitialState() { - return { - connected: false, - lastConnectAt: 0, - lastDisconnectAt: 0, - }; -} - -export default function(state = getInitialState(), action) { - if (!state.connected && action.type === GeneralTypes.WEBSOCKET_SUCCESS) { - return { - ...state, - connected: true, - lastConnectAt: new Date().getTime(), - }; - } else if (state.connected && (action.type === GeneralTypes.WEBSOCKET_FAILURE || action.type === GeneralTypes.WEBSOCKET_CLOSED)) { - return { - ...state, - connected: false, - lastDisconnectAt: new Date().getTime(), - }; - } - - if (action.type === UserTypes.LOGOUT_SUCCESS) { - return getInitialState(); - } - - return state; -} diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js index 87fd7f930..3ffce2343 100644 --- a/app/reducers/views/channel.js +++ b/app/reducers/views/channel.js @@ -266,7 +266,11 @@ function postVisibility(state = {}, action) { } case ViewTypes.INCREASE_POST_VISIBILITY: { const nextState = {...state}; - nextState[action.data] += action.amount; + if (nextState[action.data]) { + nextState[action.data] += action.amount; + } else { + nextState[action.data] = action.amount; + } return nextState; } case ViewTypes.RECEIVED_FOCUSED_POST: { diff --git a/app/reducers/views/channel.test.js b/app/reducers/views/channel.test.js new file mode 100644 index 000000000..e5fa05ff3 --- /dev/null +++ b/app/reducers/views/channel.test.js @@ -0,0 +1,112 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import channelReducer from './channel'; +import {ViewTypes} from 'app/constants'; + +describe('Reducers.channel', () => { + const initialState = { + displayName: '', + drafts: {}, + loading: false, + refreshing: false, + postCountInChannel: {}, + postVisibility: {}, + loadingPosts: {}, + lastGetPosts: {}, + retryFailed: false, + loadMorePostsVisible: true, + lastChannelViewTime: {}, + keepChannelIdAsUnread: null, + }; + + test('Initial state', () => { + const nextState = channelReducer( + { + displayName: '', + drafts: {}, + loading: false, + refreshing: false, + postCountInChannel: {}, + postVisibility: {}, + loadingPosts: {}, + lastGetPosts: {}, + retryFailed: false, + loadMorePostsVisible: true, + lastChannelViewTime: {}, + keepChannelIdAsUnread: null, + }, + {} + ); + + expect(nextState).toEqual(initialState); + }); + + test('should set the postVisibility amount for a channel', () => { + const channelId = 'channel_id'; + const amount = 15; + const nextState = channelReducer( + { + displayName: '', + drafts: {}, + loading: false, + refreshing: false, + postCountInChannel: {}, + postVisibility: {}, + loadingPosts: {}, + lastGetPosts: {}, + retryFailed: false, + loadMorePostsVisible: true, + lastChannelViewTime: {}, + keepChannelIdAsUnread: null, + }, + { + type: ViewTypes.INCREASE_POST_VISIBILITY, + data: channelId, + amount, + } + ); + + expect(nextState).toEqual({ + ...initialState, + postVisibility: { + [channelId]: amount, + }, + }); + }); + + test('should increase the postVisibility amount for a channel', () => { + const channelId = 'channel_id'; + const amount = 15; + const nextState = channelReducer( + { + displayName: '', + drafts: {}, + loading: false, + refreshing: false, + postCountInChannel: {}, + postVisibility: { + [channelId]: amount, + }, + loadingPosts: {}, + lastGetPosts: {}, + retryFailed: false, + loadMorePostsVisible: true, + lastChannelViewTime: {}, + keepChannelIdAsUnread: null, + }, + { + type: ViewTypes.INCREASE_POST_VISIBILITY, + data: channelId, + amount, + } + ); + + expect(nextState).toEqual({ + ...initialState, + postVisibility: { + [channelId]: 2 * amount, + }, + }); + }); +}); diff --git a/app/screens/channel/channel_base.js b/app/screens/channel/channel_base.js index 0a96f4250..8476eb688 100644 --- a/app/screens/channel/channel_base.js +++ b/app/screens/channel/channel_base.js @@ -14,8 +14,6 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {app} from 'app/mattermost'; - import EmptyToolbar from 'app/components/start/empty_toolbar'; import InteractiveDialogController from 'app/components/interactive_dialog_controller'; import MainSidebar from 'app/components/sidebars/main'; @@ -24,6 +22,7 @@ import SettingsSidebar from 'app/components/sidebars/settings'; import {preventDoubleTap} from 'app/utils/tap'; import PushNotifications from 'app/push_notifications'; +import EphemeralStore from 'app/store/ephemeral_store'; import tracker from 'app/utils/time_tracker'; import telemetry from 'app/telemetry'; @@ -51,6 +50,7 @@ export default class ChannelBase extends PureComponent { theme: PropTypes.object.isRequired, showTermsOfService: PropTypes.bool, disableTermsModal: PropTypes.bool, + skipMetrics: PropTypes.bool, }; static contextTypes = { @@ -93,7 +93,7 @@ export default class ChannelBase extends PureComponent { } componentDidMount() { - if (tracker.initialLoad) { + if (tracker.initialLoad && !this.props.skipMetrics) { this.props.actions.recordLoadTime('Start time', 'initialLoad'); } @@ -103,7 +103,9 @@ export default class ChannelBase extends PureComponent { EventEmitter.emit('renderDrawer'); - telemetry.end(['start:channel_screen']); + if (!this.props.skipMetrics) { + telemetry.end(['start:channel_screen']); + } } componentWillReceiveProps(nextProps) { @@ -215,8 +217,8 @@ export default class ChannelBase extends PureComponent { loadChannelsIfNecessary(teamId).then(() => { loadProfilesAndTeamMembersForDMSidebar(teamId); - if (app.startAppFromPushNotification) { - app.setStartAppFromPushNotification(false); + if (EphemeralStore.appStartedFromPushNotification) { + EphemeralStore.appStartedFromPushNotification = false; } else { selectInitialChannel(teamId); } diff --git a/app/screens/channel/channel_nav_bar/channel_nav_bar.js b/app/screens/channel/channel_nav_bar/channel_nav_bar.js index 9e203e057..1ba9afc54 100644 --- a/app/screens/channel/channel_nav_bar/channel_nav_bar.js +++ b/app/screens/channel/channel_nav_bar/channel_nav_bar.js @@ -3,9 +3,10 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Platform, View} from 'react-native'; +import {Dimensions, Platform, View} from 'react-native'; import {DeviceTypes, ViewTypes} from 'app/constants'; +import mattermostManaged from 'app/mattermost_managed'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; import ChannelDrawerButton from './channel_drawer_button'; @@ -30,6 +31,30 @@ export default class ChannelNavBar extends PureComponent { theme: PropTypes.object.isRequired, }; + state = { + isSplitView: false, + }; + + componentDidMount() { + this.mounted = true; + this.handleDimensions(); + Dimensions.addEventListener('change', this.handleDimensions); + } + + componentWillUnmount() { + this.mounted = false; + Dimensions.removeEventListener('change', this.handleDimensions); + } + + handleDimensions = () => { + if (DeviceTypes.IS_TABLET && this.mounted) { + mattermostManaged.isRunningInSplitView().then((result) => { + const isSplitView = Boolean(result.isSplitView); + this.setState({isSplitView}); + }); + } + }; + render() { const {isLandscape, onPress, theme} = this.props; const {openChannelDrawer, openSettingsDrawer} = this.props; @@ -59,7 +84,7 @@ export default class ChannelNavBar extends PureComponent { } let drawerButtonVisible = false; - if (!DeviceTypes.IS_TABLET) { + if (!DeviceTypes.IS_TABLET || this.state.isSplitView) { drawerButtonVisible = true; } diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js index dc8ca68bc..7cf52e4a1 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -33,7 +33,6 @@ export default class ChannelPostList extends PureComponent { selectPost: PropTypes.func.isRequired, recordLoadTime: PropTypes.func.isRequired, refreshChannelWithRetry: PropTypes.func.isRequired, - showModal: PropTypes.func.isRequired, goToScreen: PropTypes.func.isRequired, }).isRequired, channelId: PropTypes.string.isRequired, diff --git a/app/screens/channel_members/channel_members.js b/app/screens/channel_members/channel_members.js index ce4e7c916..9bb29b8da 100644 --- a/app/screens/channel_members/channel_members.js +++ b/app/screens/channel_members/channel_members.js @@ -246,7 +246,7 @@ export default class ChannelMembers extends PureComponent { enabled: props.id !== this.props.currentUserId, }; - this.renderItem(props, selectProps); + return this.renderItem(props, selectProps); } renderUnselectableItem = (props) => { @@ -255,7 +255,7 @@ export default class ChannelMembers extends PureComponent { enabled: false, }; - this.renderItem(props, selectProps); + return this.renderItem(props, selectProps); }; renderLoading = () => { diff --git a/app/screens/edit_profile/index.js b/app/screens/edit_profile/index.js index 81a29cb97..21eb519c7 100644 --- a/app/screens/edit_profile/index.js +++ b/app/screens/edit_profile/index.js @@ -16,7 +16,7 @@ import EditProfile from './edit_profile'; function mapStateToProps(state, ownProps) { const config = getConfig(state); const {serverVersion} = state.entities.general; - const {service} = ownProps.currentUser; + const {auth_service: service} = ownProps.currentUser; const firstNameDisabled = (service === 'ldap' && config.LdapFirstNameAttributeSet === 'true') || (service === 'saml' && config.SamlFirstNameAttributeSet === 'true'); diff --git a/app/screens/entry/entry.js b/app/screens/entry/entry.js deleted file mode 100644 index c7d576639..000000000 --- a/app/screens/entry/entry.js +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import { - View, - AppState, - Platform, -} from 'react-native'; - -import DeviceInfo from 'react-native-device-info'; - -import {setSystemEmojis} from 'mattermost-redux/actions/emojis'; -import {Client4} from 'mattermost-redux/client'; - -import { - app, - store, -} from 'app/mattermost'; -import PushNotifications from 'app/push_notifications'; -import {stripTrailingSlashes} from 'app/utils/url'; - -import ChannelLoader from 'app/components/channel_loader'; -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 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 = { - theme: PropTypes.object, - navigator: PropTypes.object, - isLandscape: PropTypes.bool, - enableTimezone: PropTypes.bool, - deviceTimezone: PropTypes.string, - actions: PropTypes.shape({ - autoUpdateTimezone: PropTypes.func.isRequired, - setDeviceToken: PropTypes.func.isRequired, - }).isRequired, - }; - - constructor(props) { - super(props); - - this.unsubscribeFromStore = null; - } - - componentDidMount() { - Client4.setUserAgent(DeviceInfo.getUserAgent()); - - if (store.getState().views.root.hydrationComplete) { - this.handleHydrationComplete(); - } else { - this.unsubscribeFromStore = store.subscribe(this.listenForHydration); - } - } - - listenForHydration = () => { - const {getState} = store; - const state = getState(); - - if (!app.isNotificationsConfigured) { - this.configurePushNotifications(); - } - - if (state.views.root.hydrationComplete) { - this.handleHydrationComplete(); - } - }; - - handleHydrationComplete = () => { - if (this.unsubscribeFromStore) { - this.unsubscribeFromStore(); - this.unsubscribeFromStore = null; - } - - this.autoUpdateTimezone(); - this.setAppCredentials(); - this.setStartupThemes(); - this.handleNotification(); - this.loadSystemEmojis(); - - if (Platform.OS === 'android') { - this.launchForAndroid(); - } else { - this.launchForiOS(); - } - }; - - autoUpdateTimezone = () => { - const { - actions: { - autoUpdateTimezone, - }, - enableTimezone, - deviceTimezone, - } = this.props; - - if (enableTimezone) { - autoUpdateTimezone(deviceTimezone); - } - }; - - 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)); - } else if (app.waitForRehydration) { - app.waitForRehydration = false; - } - - 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 - ); - }; - - handleNotification = async () => { - 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; - - // if the notification has a completed property it means that we are replying to a notification - if (completed) { - onPushNotificationReply(data, text, badge, completed); - } - PushNotifications.resetNotification(); - } - }; - - launchForAndroid = () => { - 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(); - } - }; - - loadSystemEmojis = () => { - const EmojiIndicesByAlias = require('app/utils/emojis').EmojiIndicesByAlias; - setSystemEmojis(EmojiIndicesByAlias); - }; - - render() { - const { - navigator, - isLandscape, - } = this.props; - - let toolbar = null; - let loading = null; - const backgroundColor = app.appBackground ? app.appBackground : '#ffff'; - if (app.token && app.toolbarBackground) { - const toolbarTheme = { - sidebarHeaderBg: app.toolbarBackground, - sidebarHeaderTextColor: app.toolbarTextColor, - }; - - toolbar = ( - - - - - ); - - loading = ( - - ); - } else { - loading = ; - } - - return ( - - {toolbar} - {loading} - - ); - } -} diff --git a/app/screens/entry/index.js b/app/screens/entry/index.js deleted file mode 100644 index 22bebdae8..000000000 --- a/app/screens/entry/index.js +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {bindActionCreators} from 'redux'; -import {connect} from 'react-redux'; - -import {setDeviceToken} from 'mattermost-redux/actions/general'; -import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; - -import {isLandscape} from 'app/selectors/device'; -import {getDeviceTimezone} from 'app/utils/timezone'; - -const lazyLoadEntry = () => { - return require('./entry').default; -}; - -function mapStateToProps(state) { - const enableTimezone = isTimezoneEnabled(state); - const deviceTimezone = getDeviceTimezone(); - - return { - theme: getTheme(state), - isLandscape: isLandscape(state), - enableTimezone, - deviceTimezone, - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - autoUpdateTimezone, - setDeviceToken, - }, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(lazyLoadEntry()); diff --git a/app/screens/index.js b/app/screens/index.js index e5ab8f5af..36d38b336 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -6,7 +6,6 @@ import {Navigation} from 'react-native-navigation'; import {gestureHandlerRootHOC} from 'react-native-gesture-handler'; import Channel from 'app/screens/channel'; -import Entry from 'app/screens/entry'; import Root from 'app/components/root'; import SelectServer from 'app/screens/select_server'; @@ -47,7 +46,6 @@ export function registerScreens(store, Provider) { Navigation.registerComponent('EditChannel', () => wrapper(require('app/screens/edit_channel').default), () => require('app/screens/edit_channel').default); Navigation.registerComponent('EditPost', () => wrapper(require('app/screens/edit_post').default), () => require('app/screens/edit_post').default); Navigation.registerComponent('EditProfile', () => wrapper(require('app/screens/edit_profile').default), () => require('app/screens/edit_profile').default); - Navigation.registerComponent('Entry', () => wrapper(Entry), () => Entry); Navigation.registerComponent('ExpandedAnnouncementBanner', () => wrapper(require('app/screens/expanded_announcement_banner').default), () => require('app/screens/expanded_announcement_banner').default); Navigation.registerComponent('FlaggedPosts', () => wrapper(require('app/screens/flagged_posts').default), () => require('app/screens/flagged_posts').default); Navigation.registerComponent('ForgotPassword', () => wrapper(require('app/screens/forgot_password').default), () => require('app/screens/forgot_password').default); @@ -89,4 +87,4 @@ export function registerScreens(store, Provider) { Navigation.registerComponent('TimezoneSettings', () => wrapper(require('app/screens/timezone').default), () => require('app/screens/timezone').default); Navigation.registerComponent('ErrorTeamsList', () => wrapper(require('app/screens/error_teams_list').default), () => require('app/screens/error_teams_list').default); Navigation.registerComponent('UserProfile', () => wrapper(require('app/screens/user_profile').default), () => require('app/screens/user_profile').default); -} +} \ No newline at end of file diff --git a/app/screens/long_post/index.js b/app/screens/long_post/index.js index 3352ccdca..90cb2a8fe 100644 --- a/app/screens/long_post/index.js +++ b/app/screens/long_post/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {selectPost} from 'mattermost-redux/actions/posts'; +import {selectPost, makeGetReactionsForPost} from 'mattermost-redux/actions/posts'; import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; @@ -16,14 +16,16 @@ import LongPost from './long_post'; function makeMapStateToProps() { const getChannel = makeGetChannel(); + const getReactionsForPost = makeGetReactionsForPost(); return function mapStateToProps(state, ownProps) { const post = getPost(state, ownProps.postId); const channel = post ? getChannel(state, {id: post.channel_id}) : null; + const reactions = getReactionsForPost(state, post.id); return { channelName: channel ? channel.display_name : '', - hasReactions: post ? post.has_reactions : false, + hasReactions: (reactions && Object.keys(reactions).length > 0) || Boolean(post.has_reactions), inThreadView: Boolean(state.entities.posts.selectedPostId), fileIds: post ? post.file_ids : false, theme: getTheme(state), diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index 052044aa2..a51d25539 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -28,7 +28,7 @@ import {Client4} from 'mattermost-redux/client'; import ErrorText from 'app/components/error_text'; import FormattedText from 'app/components/formatted_text'; -import fetchConfig from 'app/fetch_preconfig'; +import fetchConfig from 'app/init/fetch'; import mattermostBucket from 'app/mattermost_bucket'; import {GlobalStyles} from 'app/styles'; import {checkUpgradeType, isUpgradeAvailable} from 'app/utils/client_upgrade'; @@ -102,19 +102,19 @@ export default class SelectServer extends PureComponent { telemetry.save(); } - componentWillUpdate(nextProps, nextState) { - if (nextState.connected && nextProps.hasConfigAndLicense && !(this.state.connected && this.props.hasConfigAndLicense)) { + componentDidUpdate(prevProps, prevState) { + if (this.state.connected && this.props.hasConfigAndLicense && !(prevState.connected && prevProps.hasConfigAndLicense)) { if (LocalConfig.EnableMobileClientUpgrade) { this.props.actions.setLastUpgradeCheck(); - const {currentVersion, minVersion, latestVersion} = nextProps; + const {currentVersion, minVersion, latestVersion} = prevProps; const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion); if (isUpgradeAvailable(upgradeType)) { this.handleShowClientUpgrade(upgradeType); } else { - this.handleLoginOptions(nextProps); + this.handleLoginOptions(prevProps); } } else { - this.handleLoginOptions(nextProps); + this.handleLoginOptions(prevProps); } } } diff --git a/app/screens/settings/settings_item/style.ios.js b/app/screens/settings/settings_item/style.ios.js index ddf47bf21..bebbcfe96 100644 --- a/app/screens/settings/settings_item/style.ios.js +++ b/app/screens/settings/settings_item/style.ios.js @@ -40,7 +40,7 @@ export default makeStyleSheetFromTheme((theme) => { color: theme.centerChannelColor, flex: 1, fontSize: 17, - lineHeight: 43, + alignSelf: 'center', }, arrowContainer: { justifyContent: 'center', diff --git a/app/store/ephemeral_store.js b/app/store/ephemeral_store.js index 7e69decae..f95803088 100644 --- a/app/store/ephemeral_store.js +++ b/app/store/ephemeral_store.js @@ -3,6 +3,9 @@ class EphemeralStore { constructor() { + this.appStarted = false; + this.appStartedFromPushNotification = false; + this.deviceToken = null; this.componentIdStack = []; } diff --git a/app/store/index.js b/app/store/index.js index 46743abd2..be1155865 100644 --- a/app/store/index.js +++ b/app/store/index.js @@ -248,7 +248,6 @@ export default function configureAppStore(initialState) { type: GeneralTypes.RECEIVED_APP_CREDENTIALS, data: { url: state.entities.general.credentials.url, - token: state.entities.general.credentials.token, }, }, { diff --git a/app/store/middleware.js b/app/store/middleware.js index 6910aeef3..05a8cbbb7 100644 --- a/app/store/middleware.js +++ b/app/store/middleware.js @@ -172,6 +172,10 @@ function resetStateForNewVersion(action) { selectServer, recentEmojis, }, + websocket: { + lastConnectAt: payload.websocket?.lastConnectAt, + lastDisconnectAt: payload.websocket?.lastDisconnectAt, + }, }; return { @@ -328,6 +332,10 @@ export function cleanUpState(action, keepCurrent = false) { ...payload.views.channel, }, }, + websocket: { + lastConnectAt: payload.websocket?.lastConnectAt, + lastDisconnectAt: payload.websocket?.lastDisconnectAt, + }, }; nextState.errors = payload.errors; diff --git a/app/utils/error_handling.js b/app/utils/error_handling.js index f6597f935..64fa5a59d 100644 --- a/app/utils/error_handling.js +++ b/app/utils/error_handling.js @@ -14,6 +14,8 @@ import {logError} from 'mattermost-redux/actions/errors'; import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; import {purgeOfflineStore} from 'app/actions/views/root'; +import {DEFAULT_LOCALE, getTranslations} from 'app/i18n'; +import {t} from 'app/utils/i18n'; import { captureException, captureJSException, @@ -21,54 +23,55 @@ import { LOGGER_NATIVE, } from 'app/utils/sentry'; -import {app, store} from 'app/mattermost'; - -import {t} from 'app/utils/i18n'; - -const errorHandler = (e, isFatal) => { - if (__DEV__ && !e && !isFatal) { - // react-native-exception-handler redirects console.error to call this, and React calls - // console.error without an exception when prop type validation fails, so this ends up - // being called with no arguments when the error handler is enabled in dev mode. - return; +class JavascriptAndNativeErrorHandler { + initializeErrorHandling = (store) => { + this.store = store; + initializeSentry(); + setJSExceptionHandler(this.errorHandler, false); + setNativeExceptionHandler(this.nativeErrorHandler, false); } - console.warn('Handling Javascript error', e, isFatal); // eslint-disable-line no-console - captureJSException(e, isFatal, store); + nativeErrorHandler = (e) => { + console.warn('Handling native error ' + JSON.stringify(e)); // eslint-disable-line no-console + captureException(e, LOGGER_NATIVE, this.store); + }; - const {dispatch} = store; + errorHandler = (e, isFatal) => { + if (__DEV__ && !e && !isFatal) { + // react-native-exception-handler redirects console.error to call this, and React calls + // console.error without an exception when prop type validation fails, so this ends up + // being called with no arguments when the error handler is enabled in dev mode. + return; + } - dispatch(closeWebSocket()); + console.warn('Handling Javascript error', e, isFatal); // eslint-disable-line no-console + captureJSException(e, isFatal, this.store); - if (Client4.getUrl()) { - dispatch(logError(e)); - } + const {dispatch} = this.store; - if (isFatal && e instanceof Error) { - const translations = app.getTranslations(); + dispatch(closeWebSocket()); - Alert.alert( - translations[t('mobile.error_handler.title')], - translations[t('mobile.error_handler.description')], - [{ - text: translations[t('mobile.error_handler.button')], - onPress: () => { - // purge the store - dispatch(purgeOfflineStore()); - }, - }], - {cancelable: false} - ); - } -}; + if (Client4.getUrl()) { + dispatch(logError(e)); + } -const nativeErrorHandler = (e) => { - console.warn('Handling native error ' + JSON.stringify(e)); // eslint-disable-line no-console - captureException(e, LOGGER_NATIVE, store); -}; + if (isFatal && e instanceof Error) { + const translations = getTranslations(DEFAULT_LOCALE); -export function initializeErrorHandling() { - initializeSentry(); - setJSExceptionHandler(errorHandler, false); - setNativeExceptionHandler(nativeErrorHandler, false); + Alert.alert( + translations[t('mobile.error_handler.title')], + translations[t('mobile.error_handler.description')], + [{ + text: translations[t('mobile.error_handler.button')], + onPress: () => { + // purge the store + dispatch(purgeOfflineStore()); + }, + }], + {cancelable: false} + ); + } + }; } + +export default new JavascriptAndNativeErrorHandler(); diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js index a26781e90..4d057fb06 100644 --- a/app/utils/push_notifications.js +++ b/app/utils/push_notifications.js @@ -8,7 +8,6 @@ import {setDeviceToken} from 'mattermost-redux/actions/general'; import {getPosts} from 'mattermost-redux/actions/posts'; import {Client4} from 'mattermost-redux/client'; import {General} from 'mattermost-redux/constants'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {markChannelViewedAndRead, retryGetPostsAction} from 'app/actions/views/channel'; @@ -17,169 +16,179 @@ import { loadFromPushNotification, } from 'app/actions/views/root'; import {ViewTypes} from 'app/constants'; -import {DEFAULT_LOCALE, getLocalizedMessage} from 'app/i18n'; -import {t} from 'app/utils/i18n'; -import { - app, - store, -} from 'app/mattermost'; +import {getLocalizedMessage} from 'app/i18n'; +import {getCurrentServerUrl, getAppCredentials} from 'app/init/credentials'; import PushNotifications from 'app/push_notifications'; +import {getCurrentLocale} from 'app/selectors/i18n'; +import EphemeralStore from 'app/store/ephemeral_store'; +import {t} from 'app/utils/i18n'; -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; +class PushNotificationUtils { + constructor() { + this.configured = false; + this.replyNotificationData = null; } - const token = `${prefix}:${data.token}`; - if (state.views.root.hydrationComplete) { - app.setDeviceToken(token); - store.dispatch(setDeviceToken(token)); - } else { - app.setDeviceToken(token); - } -}; + configure = (store) => { + this.store = store; -const loadFromNotification = async (notification) => { - await store.dispatch(loadFromPushNotification(notification, true)); - if (!app.startAppFromPushNotification) { - EventEmitter.emit(ViewTypes.NOTIFICATION_TAPPED); - PushNotifications.resetNotification(); - } -}; - -const onPushNotification = async (deviceNotification) => { - const {dispatch, getState} = store; - let unsubscribeFromStore = null; - let stopLoadingNotification = false; - - // mark the app as started as soon as possible - if (!app.appStarted) { - app.setStartAppFromPushNotification(true); - } - - const {data, foreground, message, userInfo, userInteraction} = deviceNotification; - const notification = { - data, - message, + PushNotifications.configure({ + onRegister: this.onRegisterDevice, + onNotification: this.onPushNotification, + onReply: this.onPushNotificationReply, + popInitialNotification: true, + requestPermissions: true, + }); }; - if (userInfo) { - notification.localNotification = userInfo.localNotification; - } + loadFromNotification = async (notification) => { + await this.store.dispatch(loadFromPushNotification(notification, true)); - if (data.type === 'clear') { - dispatch(markChannelViewedAndRead(data.channel_id, null, false)); - } else { - // get the posts for the channel as soon as possible - retryGetPostsAction(getPosts(data.channel_id), dispatch, getState); - - if (foreground) { - EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification); - } else if (userInteraction && !notification.localNotification) { - EventEmitter.emit('close_channel_drawer'); - if (getState().views.root.hydrationComplete) { - setTimeout(() => { - loadFromNotification(notification); - }, 0); - } else { - const waitForHydration = () => { - if (getState().views.root.hydrationComplete && !stopLoadingNotification) { - stopLoadingNotification = true; - unsubscribeFromStore(); - loadFromNotification(notification); - } - }; - - unsubscribeFromStore = store.subscribe(waitForHydration); - } + if (!EphemeralStore.appStartedFromPushNotification) { + EventEmitter.emit(ViewTypes.NOTIFICATION_TAPPED); + PushNotifications.resetNotification(); } - } -}; + }; -export const onPushNotificationReply = async (data, text, badge, completed) => { - const {dispatch, getState} = store; - const state = getState(); - const reduxCurrentUser = getCurrentUser(state); - const reduxCredentialsUrl = state.entities.general.credentials.url; - const reduxCredentialsToken = state.entities.general.credentials.token; + onPushNotification = async (deviceNotification) => { + const {dispatch, getState} = this.store; + let unsubscribeFromStore = null; + let stopLoadingNotification = false; - const currentUserId = reduxCurrentUser ? reduxCurrentUser.id : app.currentUserId; - const url = reduxCredentialsUrl || app.url; - const token = reduxCredentialsToken || app.token; + // mark the app as started as soon as possible + if (!EphemeralStore.appStarted) { + EphemeralStore.appStartedFromPushNotification = true; + } - 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, + const {data, foreground, message, userInfo, userInteraction} = deviceNotification; + const notification = { + data, + message, }; - if (!Client4.getUrl()) { - // Make sure the Client has the server url set - Client4.setUrl(url); + if (userInfo) { + notification.localNotification = userInfo.localNotification; } - if (!Client4.getToken()) { - // Make sure the Client has the server token set - Client4.setToken(token); - } + if (data.type === 'clear') { + dispatch(markChannelViewedAndRead(data.channel_id, null, false)); + } else { + // get the posts for the channel as soon as possible + retryGetPostsAction(getPosts(data.channel_id), dispatch, getState); - retryGetPostsAction(getPosts(data.channel_id), dispatch, getState); - const result = await dispatch(createPostForNotificationReply(post)); - if (result.error) { - const locale = reduxCurrentUser ? reduxCurrentUser.locale : DEFAULT_LOCALE; - PushNotifications.localNotification({ - message: getLocalizedMessage(locale, t('mobile.reply_post.failed')), - userInfo: { - localNotification: true, - localTest: true, - }, - }); - console.warn('Failed to send reply to push notification', result.error); // eslint-disable-line no-console + if (foreground) { + EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification); + } else if (userInteraction && !notification.localNotification) { + EventEmitter.emit('close_channel_drawer'); + if (getState().views.root.hydrationComplete) { //TODO: Replace when realm is ready + setTimeout(() => { + this.loadFromNotification(notification); + }, 0); + } else { + const waitForHydration = () => { + if (getState().views.root.hydrationComplete && !stopLoadingNotification) { + stopLoadingNotification = true; + unsubscribeFromStore(); + this.loadFromNotification(notification); + } + }; + + unsubscribeFromStore = this.store.subscribe(waitForHydration); + } + } + } + }; + + onPushNotificationReply = async (data, text, badge, completed) => { + const {dispatch, getState} = this.store; + const state = getState(); + const credentials = await getAppCredentials(); // TODO Change to handle multiple servers + const url = await getCurrentServerUrl(); // TODO Change to handle multiple servers + const token = credentials.password; + const usernameParsed = credentials.username.split(','); + const [, currentUserId] = usernameParsed; + + 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); + } + + retryGetPostsAction(getPosts(data.channel_id), dispatch, getState); + const result = await dispatch(createPostForNotificationReply(post)); + if (result.error) { + const locale = getCurrentLocale(state); + PushNotifications.localNotification({ + message: getLocalizedMessage(locale, t('mobile.reply_post.failed')), + userInfo: { + localNotification: true, + localTest: true, + }, + }); + console.warn('Failed to send reply to push notification', result.error); // eslint-disable-line no-console + completed(); + return; + } + + if (badge >= 0) { + PushNotifications.setApplicationIconBadgeNumber(badge); + } + + dispatch(markChannelViewedAndRead(data.channel_id)); + this.replyNotificationData = null; completed(); - return; + } else { + this.replyNotificationData = { + data, + text, + badge, + completed, + }; + } + }; + + onRegisterDevice = (data) => { + const {dispatch, getState} = this.store; + let unsubscribeFromStore = null; + + 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; } - if (badge >= 0) { - PushNotifications.setApplicationIconBadgeNumber(badge); - } + EphemeralStore.deviceToken = `${prefix}:${data.token}`; - dispatch(markChannelViewedAndRead(data.channel_id)); - app.setReplyNotificationData(null); - completed(); - } else { - app.setReplyNotificationData({ - data, - text, - badge, - completed, - }); + // TODO: Remove when realm is ready + const waitForHydration = () => { + if (getState().views.root.hydrationComplete && !this.configured) { + this.configured = true; + dispatch(setDeviceToken(EphemeralStore.deviceToken)); + unsubscribeFromStore(); + } + }; + + unsubscribeFromStore = this.store.subscribe(waitForHydration); } -}; +} -export const configurePushNotifications = () => { - PushNotifications.configure({ - onRegister: onRegisterDevice, - onNotification: onPushNotification, - onReply: onPushNotificationReply, - popInitialNotification: true, - requestPermissions: true, - }); - - if (app) { - app.setIsNotificationsConfigured(true); - } -}; +export default new PushNotificationUtils(); diff --git a/app/utils/url.js b/app/utils/url.js index 296a4e62c..e0feebc8f 100644 --- a/app/utils/url.js +++ b/app/utils/url.js @@ -103,9 +103,10 @@ export function matchDeepLink(url, serverURL, siteURL) { return null; } - const linkRoot = `(?:${escapeRegex(serverURL)}|${escapeRegex(siteURL)})?`; + const linkRoot = `(?:${escapeRegex('mattermost:/')}|${escapeRegex(serverURL)}|${escapeRegex(siteURL)})?`; let match = new RegExp('^' + linkRoot + '\\/([^\\/]+)\\/channels\\/(\\S+)').exec(url); + if (match) { return {type: DeepLinkTypes.CHANNEL, teamName: match[1], channelName: match[2]}; } diff --git a/index.js b/index.js index 54e26e7aa..84ce150c3 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import 'react-native/Libraries/Core/InitializeCore'; -import {AppRegistry, DeviceEventEmitter, Platform, YellowBox} from 'react-native'; +import {AppRegistry, DeviceEventEmitter, Platform, Text, YellowBox} from 'react-native'; import 'react-native-gesture-handler'; import LocalConfig from 'assets/config'; @@ -10,16 +10,46 @@ import LocalConfig from 'assets/config'; import telemetry from 'app/telemetry'; import 'app/mattermost'; -import ShareExtension from 'share_extension/android'; -YellowBox.ignoreWarnings([ - 'Warning: componentWillMount is deprecated', - 'Warning: componentWillUpdate is deprecated', - 'Warning: componentWillReceiveProps is deprecated', -]); +if (__DEV__) { + YellowBox.ignoreWarnings([ + 'Warning: componentWillMount is deprecated', + 'Warning: componentWillUpdate is deprecated', + 'Warning: componentWillReceiveProps is deprecated', + + // Hide warnings caused by React Native (https://github.com/facebook/react-native/issues/20841) + 'Require cycle: node_modules/react-native/Libraries/Network/fetch.js', + ]); +} + +const setFontFamily = () => { + // Set a global font for Android + const defaultFontFamily = { + style: { + fontFamily: 'Roboto', + }, + }; + const TextRender = Text.render; + const initialDefaultProps = Text.defaultProps; + Text.defaultProps = { + ...initialDefaultProps, + ...defaultFontFamily, + }; + Text.render = function render(props, ...args) { + const oldProps = props; + let newProps = {...props, style: [defaultFontFamily.style, props.style]}; + try { + return Reflect.apply(TextRender, this, [newProps, ...args]); + } finally { + newProps = oldProps; + } + }; +}; if (Platform.OS === 'android') { + const ShareExtension = require('share_extension/android').default; AppRegistry.registerComponent('MattermostShare', () => ShareExtension); + setFontFamily(); if (LocalConfig.TelemetryEnabled) { const metricsSubscription = DeviceEventEmitter.addListener('nativeMetrics', (metrics) => { diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 365ff6ec5..f9480132d 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ 7F581D35221ED5C60099E66B /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F581D34221ED5C60099E66B /* NotificationService.swift */; }; 7F581D39221ED5C60099E66B /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7F581D32221ED5C60099E66B /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7F581F78221EEA7C0099E66B /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; }; + 7F5BA34722B99B7B005B05D3 /* Mattermost+RCTUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F5BA34622B99B7B005B05D3 /* Mattermost+RCTUITextView.m */; }; 7F5CA9A0208FE3B9004F91CE /* libRNDocumentPicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F5CA991208FE38F004F91CE /* libRNDocumentPicker.a */; }; 7F642DF02093533300F3165E /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F642DED2093530B00F3165E /* libRNDeviceInfo.a */; }; 7F72F2EE2211220500F98FFF /* GenericPreview.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F72F2ED2211220500F98FFF /* GenericPreview.xib */; }; @@ -826,6 +827,8 @@ 7F581D34221ED5C60099E66B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 7F581D36221ED5C60099E66B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7F581F77221EEA5A0099E66B /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; + 7F5BA34522B99B7B005B05D3 /* Mattermost+RCTUITextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Mattermost+RCTUITextView.h"; path = "Mattermost/Mattermost+RCTUITextView.h"; sourceTree = ""; }; + 7F5BA34622B99B7B005B05D3 /* Mattermost+RCTUITextView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "Mattermost+RCTUITextView.m"; path = "Mattermost/Mattermost+RCTUITextView.m"; sourceTree = ""; }; 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNDocumentPicker.xcodeproj; path = "../node_modules/react-native-document-picker/ios/RNDocumentPicker.xcodeproj"; sourceTree = ""; }; 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = ""; }; 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = ""; }; @@ -1120,6 +1123,8 @@ 7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */, 7F151D40221B069200FAD8F3 /* 0155-keys.png */, 7F292AA51E8ABB1100A450A3 /* splash.png */, + 7F5BA34522B99B7B005B05D3 /* Mattermost+RCTUITextView.h */, + 7F5BA34622B99B7B005B05D3 /* Mattermost+RCTUITextView.m */, ); name = Mattermost; sourceTree = ""; @@ -2635,6 +2640,7 @@ 7FEB10981F6101710039A015 /* BlurAppScreen.m in Sources */, 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */, 7F240ACD220D460300637665 /* MattermostBucketModule.m in Sources */, + 7F5BA34722B99B7B005B05D3 /* Mattermost+RCTUITextView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2779,7 +2785,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 198; + CURRENT_PROJECT_VERSION = 203; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; @@ -2839,7 +2845,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 198; + CURRENT_PROJECT_VERSION = 203; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index bc5a783a7..7677d8aa4 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.20.0 + 1.21.0 CFBundleSignature ???? CFBundleURLTypes @@ -34,7 +34,7 @@ CFBundleVersion - 198 + 203 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/Mattermost/Mattermost+RCTUITextView.h b/ios/Mattermost/Mattermost+RCTUITextView.h new file mode 100644 index 000000000..67057d05e --- /dev/null +++ b/ios/Mattermost/Mattermost+RCTUITextView.h @@ -0,0 +1,17 @@ +// +// Mattermost+RCTUITextView.h +// Mattermost +// +// Created by Elias Nahum on 6/18/19. +// Copyright © 2019 Facebook. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface Mattermost_RCTUITextView : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Mattermost/Mattermost+RCTUITextView.m b/ios/Mattermost/Mattermost+RCTUITextView.m new file mode 100644 index 000000000..505798bbc --- /dev/null +++ b/ios/Mattermost/Mattermost+RCTUITextView.m @@ -0,0 +1,36 @@ +// +// Mattermost+RCTUITextView.m +// Mattermost +// +// Created by Elias Nahum on 6/18/19. +// Copyright © 2019 Facebook. All rights reserved. +// + +#import "Mattermost+RCTUITextView.h" +#import "RCTUITextView.h" + +@implementation Mattermost_RCTUITextView + +@end + +@implementation RCTUITextView (DisableCopyPaste) + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + NSDictionary *response = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"com.apple.configuration.managed"]; + if(response) { + NSString *copyPasteProtection = response[@"copyAndPasteProtection"]; + BOOL prevent = action == @selector(paste:) || + action == @selector(copy:) || + action == @selector(cut:) || + action == @selector(_share:); + + if ([copyPasteProtection isEqual: @"true"] && prevent) { + return NO; + } + } + + return [super canPerformAction:action withSender:sender]; +} + +@end diff --git a/ios/Mattermost/MattermostManaged.m b/ios/Mattermost/MattermostManaged.m index 4000f5902..a96f94d2f 100644 --- a/ios/Mattermost/MattermostManaged.m +++ b/ios/Mattermost/MattermostManaged.m @@ -6,7 +6,6 @@ // See License.txt for license information. // -#import "RCTUITextView.h" #import "MattermostManaged.h" #import @@ -69,6 +68,7 @@ RCT_EXPORT_MODULE(); return @{ @"hasSafeAreaInsets": @([self hasSafeAreaInsets]), + @"appGroupIdentifier": APP_GROUP_ID }; } @@ -152,31 +152,19 @@ RCT_EXPORT_METHOD(getConfig:(RCTPromiseResolveBlock)resolve } } +RCT_EXPORT_METHOD(isRunningInSplitView:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + BOOL isRunningInFullScreen = CGRectEqualToRect( + [UIApplication sharedApplication].delegate.window.frame, + [UIApplication sharedApplication].delegate.window.screen.bounds); + resolve(@{ + @"isSplitView": @(!isRunningInFullScreen) + }); +} + RCT_EXPORT_METHOD(quitApp) { exit(0); } @end - -@implementation RCTUITextView (DisableCopyPaste) - -- (BOOL)canPerformAction:(SEL)action withSender:(id)sender -{ - NSDictionary *response = [[NSUserDefaults standardUserDefaults] dictionaryForKey:configurationKey]; - if(response) { - NSString *copyPasteProtection = response[@"copyAndPasteProtection"]; - BOOL prevent = action == @selector(paste:) || - action == @selector(copy:) || - action == @selector(cut:) || - action == @selector(_share:); - - if ([copyPasteProtection isEqual: @"true"] && prevent) { - return NO; - } - } - - return [super canPerformAction:action withSender:sender]; -} - -@end diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index 2d5ea8b45..2b1905d7d 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.20.0 + 1.21.0 CFBundleVersion - 198 + 203 NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/ios/MattermostTests/Info.plist b/ios/MattermostTests/Info.plist index c948006ef..3b893649d 100644 --- a/ios/MattermostTests/Info.plist +++ b/ios/MattermostTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.20.0 + 1.21.0 CFBundleSignature ???? CFBundleVersion - 198 + 203 diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index 55924d842..f2bcfb68e 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.20.0 + 1.21.0 CFBundleVersion - 198 + 203 NSExtension NSExtensionPointIdentifier diff --git a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj index 871083eef..35da9ccc2 100644 --- a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj +++ b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj @@ -7,7 +7,10 @@ objects = { /* Begin PBXBuildFile section */ - 7F80232C229C91AD0034D6D4 /* Constants.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FABE054221387B500D0F595 /* Constants.h */; }; + 7F80232C229C91AD0034D6D4 /* MMMConstants.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FABE054221387B500D0F595 /* MMMConstants.h */; }; + 7F82907122C295AC0035544F /* MMKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F82907022C295AC0035544F /* MMKeychainManager.m */; }; + 7F82907322C296970035544F /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F82907222C296970035544F /* Security.framework */; }; + 7F82907522C296A10035544F /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F82907422C296A10035544F /* LocalAuthentication.framework */; }; 7FABE04E2213818A00D0F595 /* MattermostBucket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE04C2213818900D0F595 /* MattermostBucket.m */; }; 7FABE055221387B500D0F595 /* MMMConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE053221387B400D0F595 /* MMMConstants.m */; }; 7FABE058221388D700D0F595 /* UploadSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE057221388D600D0F595 /* UploadSessionManager.swift */; }; @@ -25,13 +28,17 @@ dstPath = "include/$(PRODUCT_NAME)"; dstSubfolderSpec = 16; files = ( - 7F80232C229C91AD0034D6D4 /* Constants.h in CopyFiles */, + 7F80232C229C91AD0034D6D4 /* MMMConstants.h in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 7F82906F22C295AB0035544F /* MMKeychainManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MMKeychainManager.h; sourceTree = ""; }; + 7F82907022C295AC0035544F /* MMKeychainManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMKeychainManager.m; sourceTree = ""; }; + 7F82907222C296970035544F /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 7F82907422C296A10035544F /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; }; 7FABE03622137F2900D0F595 /* libUploadAttachments.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libUploadAttachments.a; sourceTree = BUILT_PRODUCTS_DIR; }; 7FABE04B2213818900D0F595 /* UploadAttachments-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UploadAttachments-Bridging-Header.h"; sourceTree = ""; }; 7FABE04C2213818900D0F595 /* MattermostBucket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MattermostBucket.m; sourceTree = ""; }; @@ -52,17 +59,29 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7F82907522C296A10035544F /* LocalAuthentication.framework in Frameworks */, + 7F82907322C296970035544F /* Security.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 7F82901A22C28F770035544F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7F82907422C296A10035544F /* LocalAuthentication.framework */, + 7F82907222C296970035544F /* Security.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 7FABE02D22137F2900D0F595 = { isa = PBXGroup; children = ( 7FABE03822137F2900D0F595 /* UploadAttachments */, 7FABE03722137F2900D0F595 /* Products */, + 7F82901A22C28F770035544F /* Frameworks */, ); sourceTree = ""; }; @@ -77,6 +96,8 @@ 7FABE03822137F2900D0F595 /* UploadAttachments */ = { isa = PBXGroup; children = ( + 7F82906F22C295AB0035544F /* MMKeychainManager.h */, + 7F82907022C295AC0035544F /* MMKeychainManager.m */, 7FABE05A2213892100D0F595 /* AttachmentArray.swift */, 7FABE0592213892100D0F595 /* AttachmentItem.swift */, 7FABE054221387B500D0F595 /* MMMConstants.h */, @@ -175,6 +196,7 @@ files = ( 7FABE05C2213892200D0F595 /* AttachmentArray.swift in Sources */, 7FABE0FA2214674200D0F595 /* StoreManager.m in Sources */, + 7F82907122C295AC0035544F /* MMKeychainManager.m in Sources */, 7FABE058221388D700D0F595 /* UploadSessionManager.swift in Sources */, 7FABE0F7221466F900D0F595 /* UploadManager.swift in Sources */, 7FABE04E2213818A00D0F595 /* MattermostBucket.m in Sources */, @@ -309,6 +331,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = UQ8HT4Q2XM; + HEADER_SEARCH_PATHS = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -331,6 +354,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = UQ8HT4Q2XM; + HEADER_SEARCH_PATHS = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/UploadAttachments/UploadAttachments/MMKeychainManager.h b/ios/UploadAttachments/UploadAttachments/MMKeychainManager.h new file mode 100644 index 000000000..13489b9db --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/MMKeychainManager.h @@ -0,0 +1,3 @@ +@interface MMKeychainManager : NSObject +-(NSDictionary *)getInternetCredentialsForServer:(NSString *)server withOptions:(NSDictionary *)options; +@end diff --git a/ios/UploadAttachments/UploadAttachments/MMKeychainManager.m b/ios/UploadAttachments/UploadAttachments/MMKeychainManager.m new file mode 100644 index 000000000..866ce90fb --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/MMKeychainManager.m @@ -0,0 +1,73 @@ +#import +#import "MMKeychainManager.h" + +#import +#import + +@implementation MMKeychainManager + +// Messages from the comments in + +#pragma mark - Proposed functionality - Helpers + +#define kAuthenticationType @"authenticationType" +#define kAuthenticationTypeBiometrics @"AuthenticationWithBiometrics" + +#define kAccessControlType @"accessControl" +#define kAccessControlUserPresence @"UserPresence" +#define kAccessControlBiometryAny @"BiometryAny" +#define kAccessControlBiometryCurrentSet @"BiometryCurrentSet" +#define kAccessControlDevicePasscode @"DevicePasscode" +#define kAccessControlApplicationPassword @"ApplicationPassword" +#define kAccessControlBiometryAnyOrDevicePasscode @"BiometryAnyOrDevicePasscode" +#define kAccessControlBiometryCurrentSetOrDevicePasscode @"BiometryCurrentSetOrDevicePasscode" + +#define kBiometryTypeTouchID @"TouchID" +#define kBiometryTypeFaceID @"FaceID" + +#define kAuthenticationPromptMessage @"authenticationPrompt" + +#pragma mark - Native access + +-(NSDictionary *)getInternetCredentialsForServer:(NSString *)server withOptions:(NSDictionary *)options +{ + if (server == nil) { + return nil; + } + + NSDictionary *query = @{ + (__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword), + (__bridge NSString *)kSecAttrServer: server, + (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne + }; + + // Look up server in the keychain + NSDictionary *found = nil; + CFTypeRef foundTypeRef = NULL; + OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef) query, (CFTypeRef*)&foundTypeRef); + + if (osStatus != noErr && osStatus != errSecItemNotFound) { + return nil; + } + + found = (__bridge NSDictionary*)(foundTypeRef); + if (!found) { + return nil; + } + + // Found + NSString *username = (NSString *) [found objectForKey:(__bridge id)(kSecAttrAccount)]; + NSString *password = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding]; + + CFRelease(foundTypeRef); + NSDictionary *result = @{ + @"server": server, + @"username": username, + @"password": password + }; + return result; +} + +@end diff --git a/ios/UploadAttachments/UploadAttachments/MattermostBucket.m b/ios/UploadAttachments/UploadAttachments/MattermostBucket.m index a9b99de04..8be32f6a3 100644 --- a/ios/UploadAttachments/UploadAttachments/MattermostBucket.m +++ b/ios/UploadAttachments/UploadAttachments/MattermostBucket.m @@ -32,8 +32,13 @@ if(![fileManager fileExistsAtPath:filePath]) { return nil; } + NSData *data = [NSData dataWithContentsOfFile:filePath]; - return [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; + if (data != nil) { + return [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; + } + + return nil; } -(void)removeFile:(NSString *)fileName { diff --git a/ios/UploadAttachments/UploadAttachments/StoreManager.h b/ios/UploadAttachments/UploadAttachments/StoreManager.h index ce4e4f5eb..23fb17be6 100644 --- a/ios/UploadAttachments/UploadAttachments/StoreManager.h +++ b/ios/UploadAttachments/UploadAttachments/StoreManager.h @@ -1,8 +1,10 @@ #import #import "MattermostBucket.h" +#import "MMKeychainManager.h" @interface StoreManager : NSObject @property MattermostBucket *bucket; +@property MMKeychainManager *keychain; @property (nonatomic, strong) NSDictionary *entities; +(instancetype)shared; diff --git a/ios/UploadAttachments/UploadAttachments/StoreManager.m b/ios/UploadAttachments/UploadAttachments/StoreManager.m index 143f37a5d..bdaddae1b 100644 --- a/ios/UploadAttachments/UploadAttachments/StoreManager.m +++ b/ios/UploadAttachments/UploadAttachments/StoreManager.m @@ -15,8 +15,9 @@ -(instancetype)init { self = [super init]; if (self) { - self.bucket = [[MattermostBucket alloc] init]; - [self getEntities:true]; + self.bucket = [[MattermostBucket alloc] init]; + self.keychain = [[MMKeychainManager alloc] init]; + [self getEntities:true]; } return self; } @@ -150,10 +151,12 @@ } -(NSString *)getToken { - NSDictionary *general = [self.entities objectForKey:@"general"]; - NSDictionary *credentials = [general objectForKey:@"credentials"]; + NSDictionary *options = @{ + @"accessGroup": APP_GROUP_ID + }; + NSDictionary *credentials = [self.keychain getInternetCredentialsForServer:[self getServerUrl] withOptions:options]; - return [credentials objectForKey:@"token"]; + return [credentials objectForKey:@"password"]; } -(UInt64)scanValueFromConfig:(NSDictionary *)config key:(NSString *)key { diff --git a/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h b/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h index 194d712c7..ec1e7cbcc 100644 --- a/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h +++ b/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h @@ -5,3 +5,4 @@ #import "MMMConstants.h" #import "MattermostBucket.h" #import "StoreManager.h" +#import "MMKeychainManager.h" diff --git a/ios/UploadAttachments/UploadAttachments/UploadSession.swift b/ios/UploadAttachments/UploadAttachments/UploadSession.swift index de1b51726..8a9c5b64f 100644 --- a/ios/UploadAttachments/UploadAttachments/UploadSession.swift +++ b/ios/UploadAttachments/UploadAttachments/UploadSession.swift @@ -126,28 +126,30 @@ import os.log public func notificationReceipt(notificationId: Any?, receivedAt: Int, type: Any?) { let store = StoreManager.shared() as StoreManager - let _ = store.getEntities(true) - let serverURL = store.getServerUrl() - let sessionToken = store.getToken() - let urlString = "\(serverURL!)/api/v4/notifications/ack" - - if (notificationId != nil) { - let jsonObject: [String: Any] = [ - "id": notificationId as Any, - "received_at": receivedAt, - "platform": "ios", - "type": type as Any - ] + let entities = store.getEntities(true) + if (entities != nil) { + let serverURL = store.getServerUrl() + let sessionToken = store.getToken() + let urlString = "\(serverURL!)/api/v4/notifications/ack" - if !JSONSerialization.isValidJSONObject(jsonObject) {return} - - guard let url = URL(string: urlString) else {return} - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("Bearer \(sessionToken!)", forHTTPHeaderField: "Authorization") - request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") - request.httpBody = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) - URLSession(configuration: .ephemeral).dataTask(with: request).resume() + if (notificationId != nil) { + let jsonObject: [String: Any] = [ + "id": notificationId as Any, + "received_at": receivedAt, + "platform": "ios", + "type": type as Any + ] + + if !JSONSerialization.isValidJSONObject(jsonObject) {return} + + guard let url = URL(string: urlString) else {return} + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(sessionToken!)", forHTTPHeaderField: "Authorization") + request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") + request.httpBody = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) + URLSession(configuration: .ephemeral).dataTask(with: request).resume() + } } } } diff --git a/package-lock.json b/package-lock.json index f2058a1c8..d924b3d8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mattermost-mobile", - "version": "1.20.0", + "version": "1.21.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -5238,6 +5238,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -5282,7 +5283,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "dev": true, + "optional": true }, "is-glob": { "version": "4.0.1", @@ -7667,24 +7669,25 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "resolved": "", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": false, + "resolved": "", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "optional": true, "requires": { @@ -7694,13 +7697,15 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "resolved": "", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": false, + "resolved": "", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7708,34 +7713,37 @@ }, "chownr": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "resolved": "", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "resolved": "", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "resolved": "", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, "debug": { "version": "4.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "optional": true, "requires": { @@ -7744,25 +7752,25 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": false, + "resolved": "", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "optional": true }, "delegates": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, "fs-minipass": { "version": "1.2.5", - "resolved": false, + "resolved": "", "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "optional": true, "requires": { @@ -7771,13 +7779,13 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "optional": true }, "gauge": { "version": "2.7.4", - "resolved": false, + "resolved": "", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "optional": true, "requires": { @@ -7793,7 +7801,7 @@ }, "glob": { "version": "7.1.3", - "resolved": false, + "resolved": "", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "optional": true, "requires": { @@ -7807,13 +7815,13 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": false, + "resolved": "", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "optional": true, "requires": { @@ -7822,7 +7830,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "optional": true, "requires": { @@ -7831,7 +7839,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { @@ -7841,46 +7849,51 @@ }, "inherits": { "version": "2.0.3", - "resolved": false, - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "resolved": "", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "ini": { "version": "1.3.5", - "resolved": false, + "resolved": "", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": false, + "resolved": "", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "resolved": false, - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "resolved": "", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true }, "minipass": { "version": "2.3.5", - "resolved": false, + "resolved": "", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7888,7 +7901,7 @@ }, "minizlib": { "version": "1.2.1", - "resolved": false, + "resolved": "", "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "optional": true, "requires": { @@ -7897,21 +7910,22 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": false, + "resolved": "", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, "requires": { "minimist": "0.0.8" } }, "ms": { "version": "2.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "optional": true }, "needle": { "version": "2.3.0", - "resolved": false, + "resolved": "", "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "optional": true, "requires": { @@ -7922,7 +7936,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": false, + "resolved": "", "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "optional": true, "requires": { @@ -7940,7 +7954,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "optional": true, "requires": { @@ -7950,13 +7964,13 @@ }, "npm-bundled": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "optional": true }, "npm-packlist": { "version": "1.4.1", - "resolved": false, + "resolved": "", "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "optional": true, "requires": { @@ -7966,7 +7980,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "optional": true, "requires": { @@ -7978,38 +7992,40 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "resolved": "", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "optional": true }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "optional": true }, "osenv": { "version": "0.1.5", - "resolved": false, + "resolved": "", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "optional": true, "requires": { @@ -8019,19 +8035,19 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "optional": true }, "rc": { "version": "1.2.8", - "resolved": false, + "resolved": "", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "optional": true, "requires": { @@ -8043,7 +8059,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": false, + "resolved": "", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "optional": true } @@ -8051,7 +8067,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "optional": true, "requires": { @@ -8066,7 +8082,7 @@ }, "rimraf": { "version": "2.6.3", - "resolved": false, + "resolved": "", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "optional": true, "requires": { @@ -8075,43 +8091,45 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": false, - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "resolved": "", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "optional": true }, "sax": { "version": "1.2.4", - "resolved": false, + "resolved": "", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "optional": true }, "semver": { "version": "5.7.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "optional": true }, "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8120,7 +8138,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "requires": { @@ -8129,21 +8147,22 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "optional": true }, "tar": { "version": "4.4.8", - "resolved": false, + "resolved": "", "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "optional": true, "requires": { @@ -8158,13 +8177,13 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": false, + "resolved": "", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "optional": true, "requires": { @@ -8173,13 +8192,15 @@ }, "wrappy": { "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "resolved": "", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.3", - "resolved": false, - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "resolved": "", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "optional": true } } }, @@ -12563,9 +12584,9 @@ "dev": true }, "jsc-android": { - "version": "241213.1.0", - "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-241213.1.0.tgz", - "integrity": "sha512-AH8NYyMNLNhcUEF97QbMxPNLNW+oiSBlvm1rsMNzgJ1d5TQzdh/AOJGsxeeESp3m9YIWGLCgUvGTVoVLs0p68A==" + "version": "241213.2.0", + "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-241213.2.0.tgz", + "integrity": "sha512-nfddejB9jxFSG+Uewf+zwATFi8F2CZEEgoHLoOj13egiBDoC7zMoxK1c5/Ycf3AGmGuwCgjpn3LWe0f4tKYbjw==" }, "jsdom": { "version": "11.12.0", @@ -13139,8 +13160,8 @@ "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==" }, "mattermost-redux": { - "version": "github:mattermost/mattermost-redux#40cecab5d74708d651460515b24115fd75fb334e", - "from": "github:mattermost/mattermost-redux#40cecab5d74708d651460515b24115fd75fb334e", + "version": "github:mattermost/mattermost-redux#66dcea7c71de9f87e3fe12652458e16f9517f6b7", + "from": "github:mattermost/mattermost-redux#66dcea7c71de9f87e3fe12652458e16f9517f6b7", "requires": { "deep-equal": "1.0.1", "eslint-plugin-header": "3.0.0", @@ -14165,7 +14186,7 @@ "dependencies": { "ansi-regex": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, @@ -14177,7 +14198,7 @@ }, "cliui": { "version": "4.1.0", - "resolved": false, + "resolved": "", "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", "dev": true, "requires": { @@ -14206,7 +14227,7 @@ }, "find-up": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { @@ -14221,13 +14242,13 @@ }, "invert-kv": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "dev": true }, "lcid": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", "dev": true, "requires": { @@ -14236,7 +14257,7 @@ }, "locate-path": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { @@ -14263,7 +14284,7 @@ }, "os-locale": { "version": "3.1.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "dev": true, "requires": { @@ -14283,7 +14304,7 @@ }, "p-locate": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "requires": { @@ -14304,7 +14325,7 @@ }, "resolve-from": { "version": "4.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, @@ -14338,7 +14359,7 @@ }, "strip-ansi": { "version": "4.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { @@ -14347,13 +14368,13 @@ }, "uuid": { "version": "3.3.2", - "resolved": false, + "resolved": "", "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "dev": true }, "y18n": { "version": "4.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "dev": true }, @@ -16035,7 +16056,8 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true + "dev": true, + "optional": true }, "braces": { "version": "2.3.2", @@ -16298,7 +16320,8 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true + "dev": true, + "optional": true }, "micromatch": { "version": "3.1.10", diff --git a/package.json b/package.json index 9cdb3d4cd..d6280e23d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mattermost-mobile", - "version": "1.20.0", + "version": "1.21.0", "description": "Mattermost Mobile with React Native", "repository": "git@github.com:mattermost/mattermost-mobile.git", "author": "Mattermost, Inc.", @@ -19,8 +19,8 @@ "fuse.js": "3.4.4", "intl": "1.2.5", "jail-monkey": "2.2.0", - "jsc-android": "241213.1.0", - "mattermost-redux": "github:mattermost/mattermost-redux#40cecab5d74708d651460515b24115fd75fb334e", + "jsc-android": "241213.2.0", + "mattermost-redux": "github:mattermost/mattermost-redux#66dcea7c71de9f87e3fe12652458e16f9517f6b7", "mime-db": "1.40.0", "moment-timezone": "0.5.25", "prop-types": "15.7.2", diff --git a/packager/moduleNames.js b/packager/moduleNames.js index a19dc859d..17572aadf 100644 --- a/packager/moduleNames.js +++ b/packager/moduleNames.js @@ -2,88 +2,8 @@ // See LICENSE.txt for license information. module.exports = [ - 'app/app.js', - 'app/components/app_icon.js', - 'app/components/at_mention/at_mention.js', - 'app/components/at_mention/index.js', - 'app/components/attachment_button.js', - 'app/components/badge.js', - 'app/components/emoji/emoji.js', - 'app/components/emoji/index.js', - 'app/components/fade.js', - 'app/components/file_attachment_list/file_attachment_icon.js', - 'app/components/file_attachment_list/file_attachment_image.js', - 'app/components/formatted_date.js', - 'app/components/formatted_markdown_text.js', - 'app/components/formatted_text.js', - 'app/components/formatted_time.js', - 'app/components/interactive_dialog_controller/index.js', - 'app/components/interactive_dialog_controller/interactive_dialog_controller.js', - 'app/components/layout/keyboard_layout/index.js', - 'app/components/layout/keyboard_layout/keyboard_layout.js', 'app/components/loading.js', - 'app/components/markdown/hashtag/hashtag.js', - 'app/components/markdown/hashtag/index.js', - 'app/components/markdown/index.js', - 'app/components/markdown/markdown.js', - 'app/components/markdown/markdown_block_quote.js', - 'app/components/markdown/markdown_code_block/index.js', - 'app/components/markdown/markdown_code_block/markdown_code_block.js', - 'app/components/markdown/markdown_emoji/index.js', - 'app/components/markdown/markdown_emoji/markdown_emoji.js', - 'app/components/markdown/markdown_image/index.js', - 'app/components/markdown/markdown_image/markdown_image.js', - 'app/components/markdown/markdown_link/index.js', - 'app/components/markdown/markdown_link/markdown_link.js', - 'app/components/markdown/markdown_list.js', - 'app/components/markdown/markdown_list_item.js', - 'app/components/markdown/markdown_table/index.js', - 'app/components/markdown/markdown_table/markdown_table.js', - 'app/components/markdown/markdown_table_cell/index.js', - 'app/components/markdown/markdown_table_cell/markdown_table_cell.js', - 'app/components/markdown/markdown_table_image/index.js', - 'app/components/markdown/markdown_table_image/markdown_table_image.js', - 'app/components/markdown/markdown_table_row/index.js', - 'app/components/markdown/markdown_table_row/markdown_table_row.js', - 'app/components/network_indicator/index.js', - 'app/components/network_indicator/network_indicator.js', 'app/components/paper_plane.js', - 'app/components/post/index.js', - 'app/components/post/post.js', - 'app/components/post_body/index.js', - 'app/components/post_body/post_body.js', - 'app/components/post_header/index.js', - 'app/components/post_header/post_header.js', - 'app/components/post_header/post_pre_header.js', - 'app/components/post_list/date_header/date_header.js', - 'app/components/post_list/date_header/index.js', - 'app/components/post_list/index.js', - 'app/components/post_list/new_messages_divider.js', - 'app/components/post_list/post_list.js', - 'app/components/post_list_retry.js', - 'app/components/post_profile_picture/index.js', - 'app/components/post_profile_picture/post_profile_picture.js', - 'app/components/post_textbox/components/typing/index.js', - 'app/components/post_textbox/components/typing/typing.js', - 'app/components/post_textbox/index.js', - 'app/components/post_textbox/post_textbox.android.js', - 'app/components/post_textbox/post_textbox_base.js', - 'app/components/profile_picture/index.js', - 'app/components/profile_picture/profile_picture.js', - 'app/components/progressive_image/index.js', - 'app/components/progressive_image/progressive_image.js', - 'app/components/reply_icon.js', - 'app/components/safe_area_view/index.js', - 'app/components/safe_area_view/safe_area_view.android.js', - 'app/components/send_button.js', - 'app/components/show_more_button/index.js', - 'app/components/show_more_button/show_more_button.js', - 'app/components/start/empty_toolbar.js', - 'app/components/status_bar/index.js', - 'app/components/status_bar/status_bar.js', - 'app/components/user_status/index.js', - 'app/components/user_status/user_status.js', - 'app/components/vector_icon.js', 'app/constants/custom_prop_types.js', 'app/constants/deep_linking.js', 'app/constants/device.js', @@ -92,7 +12,10 @@ module.exports = [ 'app/constants/navigation.js', 'app/constants/permissions.js', 'app/constants/view.js', - 'app/fetch_preconfig.js', + 'app/init/credentials.js', + 'app/init/emm_provider.js', + 'app/init/fetch.js', + 'app/init/global_event_handler.js', 'app/initial_state.js', 'app/mattermost.js', 'app/mattermost_bucket/index.js', @@ -109,7 +32,6 @@ module.exports = [ 'app/reducers/device/is_tablet.js', 'app/reducers/device/orientation.js', 'app/reducers/device/status_bar.js', - 'app/reducers/device/websocket.js', 'app/reducers/index.js', 'app/reducers/navigation/index.js', 'app/reducers/views/announcement.js', @@ -128,61 +50,17 @@ module.exports = [ 'app/reducers/views/team.js', 'app/reducers/views/thread.js', 'app/reducers/views/user.js', - 'app/screens/channel/channel.android.js', - 'app/screens/channel/channel_base.js', - 'app/screens/channel/channel_nav_bar/channel_drawer_button.js', - 'app/screens/channel/channel_nav_bar/channel_nav_bar.js', - 'app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js', - 'app/screens/channel/channel_nav_bar/channel_search_button/index.js', - 'app/screens/channel/channel_nav_bar/channel_title/channel_title.js', - 'app/screens/channel/channel_nav_bar/channel_title/index.js', - 'app/screens/channel/channel_nav_bar/index.js', - 'app/screens/channel/channel_nav_bar/settings_drawer_button.js', - 'app/screens/channel/channel_post_list/channel_post_list.js', - 'app/screens/channel/channel_post_list/index.js', - 'app/screens/channel/index.js', - 'app/screens/entry/entry.js', - 'app/screens/entry/index.js', - 'app/screens/index.js', - 'app/screens/select_server/index.js', - 'app/screens/select_server/select_server.js', - 'app/selectors/client_upgrade.js', + 'app/store/ephemeral_store.js', 'app/store/index.js', 'app/store/middleware.js', 'app/store/thunk.js', 'app/telemetry/index.js', 'app/telemetry/telemetry.android.js', - 'app/telemetry/telemetry_utils.js', - 'app/utils/avoid_native_bridge.js', - 'app/utils/general.js', - 'app/utils/i18n.js', 'app/utils/image_cache_manager.js', - 'app/utils/network.js', 'app/utils/push_notifications.js', 'app/utils/sentry/middleware.js', 'app/utils/theme.js', - 'app/utils/time_tracker.js', 'dist/assets/config.json', - 'dist/assets/images/icons/audio.png', - 'dist/assets/images/icons/brokenimage.png', - 'dist/assets/images/icons/code.png', - 'dist/assets/images/icons/excel.png', - 'dist/assets/images/icons/generic.png', - 'dist/assets/images/icons/image.png', - 'dist/assets/images/icons/patch.png', - 'dist/assets/images/icons/pdf.png', - 'dist/assets/images/icons/ppt.png', - 'dist/assets/images/icons/video.png', - 'dist/assets/images/icons/webhook.jpg', - 'dist/assets/images/icons/word.png', - 'dist/assets/images/post_header/flag.png', - 'dist/assets/images/post_header/pin.png', - 'dist/assets/images/profile.jpg', - 'dist/assets/images/status/away.png', - 'dist/assets/images/status/dnd.png', - 'dist/assets/images/status/offline.png', - 'dist/assets/images/status/online.png', - 'dist/assets/images/thumb.png', 'index.js', 'node_modules/@babel/runtime/helpers/arrayWithHoles.js', 'node_modules/@babel/runtime/helpers/assertThisInitialized.js', @@ -208,9 +86,7 @@ module.exports = [ 'node_modules/@babel/runtime/regenerator/index.js', 'node_modules/@react-native-community/async-storage/lib/AsyncStorage.js', 'node_modules/@react-native-community/async-storage/lib/index.js', - 'node_modules/@react-native-community/netinfo/js/index.js', 'node_modules/base-64/base64.js', - 'node_modules/commonmark-react-renderer/src/commonmark-react-renderer.js', 'node_modules/component-emitter/index.js', 'node_modules/core-js/modules/_a-function.js', 'node_modules/core-js/modules/_add-to-unscopables.js', @@ -288,12 +164,10 @@ module.exports = [ 'node_modules/core-js/modules/es6.set.js', 'node_modules/core-js/modules/es6.string.includes.js', 'node_modules/core-js/modules/es6.string.iterator.js', - 'node_modules/core-js/modules/es6.string.starts-with.js', 'node_modules/core-js/modules/es6.symbol.js', 'node_modules/core-js/modules/es7.array.includes.js', 'node_modules/core-js/modules/es7.object.define-getter.js', 'node_modules/core-js/modules/es7.object.define-setter.js', - 'node_modules/core-js/modules/es7.object.entries.js', 'node_modules/core-js/modules/es7.object.values.js', 'node_modules/core-js/modules/es7.symbol.async-iterator.js', 'node_modules/core-js/modules/web.dom.iterable.js', @@ -311,7 +185,6 @@ module.exports = [ 'node_modules/fbjs/lib/keyOf.js', 'node_modules/fbjs/lib/performance.js', 'node_modules/fbjs/lib/performanceNow.js', - 'node_modules/fuse.js/dist/fuse.js', 'node_modules/global/window.js', 'node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js', 'node_modules/intl-format-cache/index.js', @@ -473,10 +346,6 @@ module.exports = [ 'node_modules/mattermost-redux/reducers/requests/search.js', 'node_modules/mattermost-redux/reducers/requests/teams.js', 'node_modules/mattermost-redux/reducers/requests/users.js', - 'node_modules/mattermost-redux/selectors/entities/emojis.js', - 'node_modules/mattermost-redux/selectors/entities/general.js', - 'node_modules/mattermost-redux/selectors/entities/integrations.js', - 'node_modules/mattermost-redux/selectors/entities/teams.js', 'node_modules/mattermost-redux/store/configureStore.prod.js', 'node_modules/mattermost-redux/store/helpers.js', 'node_modules/mattermost-redux/store/index.js', @@ -485,17 +354,9 @@ module.exports = [ 'node_modules/mattermost-redux/store/reducer_registry.js', 'node_modules/mattermost-redux/utils/deep_freeze.js', 'node_modules/mattermost-redux/utils/event_emitter.js', - 'node_modules/mattermost-redux/utils/file_utils.js', - 'node_modules/mattermost-redux/utils/helpers.js', 'node_modules/mattermost-redux/utils/key_mirror.js', - 'node_modules/mattermost-redux/utils/post_list.js', 'node_modules/mattermost-redux/utils/theme_utils.js', - 'node_modules/moment-timezone/data/packed/latest.json', - 'node_modules/moment-timezone/index.js', - 'node_modules/moment-timezone/moment-timezone.js', - 'node_modules/moment/moment.js', 'node_modules/object-assign/index.js', - 'node_modules/pascalcase/index.js', 'node_modules/path-to-regexp/index.js', 'node_modules/path-to-regexp/node_modules/isarray/index.js', 'node_modules/promise/setimmediate/core.js', @@ -517,11 +378,8 @@ module.exports = [ 'node_modules/react-intl/locale-data/index.js', 'node_modules/react-is/cjs/react-is.production.min.js', 'node_modules/react-is/index.js', - 'node_modules/react-native-button/Button.js', - 'node_modules/react-native-button/coalesceNonElementChildren.js', 'node_modules/react-native-device-info/deviceinfo.js', - 'node_modules/react-native-linear-gradient/common.js', - 'node_modules/react-native-linear-gradient/index.android.js', + 'node_modules/react-native-keychain/index.js', 'node_modules/react-native-local-auth/LocalAuth.android.js', 'node_modules/react-native-local-auth/index.js', 'node_modules/react-native-navigation/lib/dist/Navigation.js', @@ -553,7 +411,6 @@ module.exports = [ 'node_modules/react-native/Libraries/Animated/src/AnimatedImplementation.js', 'node_modules/react-native/Libraries/Animated/src/Easing.js', 'node_modules/react-native/Libraries/Animated/src/components/AnimatedText.js', - 'node_modules/react-native/Libraries/Animated/src/components/AnimatedView.js', 'node_modules/react-native/Libraries/Animated/src/createAnimatedComponent.js', 'node_modules/react-native/Libraries/Animated/src/nodes/AnimatedInterpolation.js', 'node_modules/react-native/Libraries/Animated/src/nodes/AnimatedNode.js', @@ -574,13 +431,9 @@ module.exports = [ 'node_modules/react-native/Libraries/Components/TextInput/TextInputState.js', 'node_modules/react-native/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js', 'node_modules/react-native/Libraries/Components/Touchable/Touchable.js', - 'node_modules/react-native/Libraries/Components/Touchable/TouchableOpacity.js', - 'node_modules/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js', 'node_modules/react-native/Libraries/Components/View/PlatformViewPropTypes.android.js', 'node_modules/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js', 'node_modules/react-native/Libraries/Components/View/ReactNativeViewAttributes.js', - 'node_modules/react-native/Libraries/Components/View/View.js', - 'node_modules/react-native/Libraries/Components/View/ViewNativeComponent.js', 'node_modules/react-native/Libraries/Core/ExceptionsManager.js', 'node_modules/react-native/Libraries/Core/InitializeCore.js', 'node_modules/react-native/Libraries/Core/ReactNativeVersion.js', @@ -617,7 +470,6 @@ module.exports = [ 'node_modules/react-native/Libraries/Image/AssetRegistry.js', 'node_modules/react-native/Libraries/Image/AssetSourceResolver.js', 'node_modules/react-native/Libraries/Image/Image.android.js', - 'node_modules/react-native/Libraries/Image/ImageBackground.js', 'node_modules/react-native/Libraries/Image/resolveAssetSource.js', 'node_modules/react-native/Libraries/Interaction/PanResponder.js', 'node_modules/react-native/Libraries/JSInspector/InspectorAgent.js', @@ -626,10 +478,8 @@ module.exports = [ 'node_modules/react-native/Libraries/Linking/Linking.js', 'node_modules/react-native/Libraries/Network/NetInfo.js', 'node_modules/react-native/Libraries/Performance/Systrace.js', - 'node_modules/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js', 'node_modules/react-native/Libraries/Promise.js', 'node_modules/react-native/Libraries/ReactNative/AppRegistry.js', - 'node_modules/react-native/Libraries/ReactNative/FabricUIManager.js', 'node_modules/react-native/Libraries/ReactNative/I18nManager.js', 'node_modules/react-native/Libraries/ReactNative/UIManager.js', 'node_modules/react-native/Libraries/ReactNative/UIManagerProperties.js', @@ -646,7 +496,6 @@ module.exports = [ 'node_modules/react-native/Libraries/StyleSheet/processTransform.js', 'node_modules/react-native/Libraries/Text/Text.js', 'node_modules/react-native/Libraries/Text/TextStylePropTypes.js', - 'node_modules/react-native/Libraries/Utilities/BackHandler.android.js', 'node_modules/react-native/Libraries/Utilities/DeviceInfo.js', 'node_modules/react-native/Libraries/Utilities/Dimensions.js', 'node_modules/react-native/Libraries/Utilities/HMRClient.js', @@ -730,15 +579,30 @@ module.exports = [ 'node_modules/redux-thunk/lib/index.js', 'node_modules/redux/lib/redux.js', 'node_modules/regenerator-runtime/runtime.js', - 'node_modules/reselect/lib/index.js', 'node_modules/rn-fetch-blob/android.js', 'node_modules/rn-fetch-blob/cba/index.js', 'node_modules/rn-fetch-blob/class/RNFetchBlobFile.js', + 'node_modules/rn-fetch-blob/class/RNFetchBlobReadStream.js', + 'node_modules/rn-fetch-blob/class/RNFetchBlobSession.js', + 'node_modules/rn-fetch-blob/class/RNFetchBlobWriteStream.js', 'node_modules/rn-fetch-blob/fs.js', 'node_modules/rn-fetch-blob/index.js', 'node_modules/rn-fetch-blob/json-stream.js', 'node_modules/rn-fetch-blob/lib/oboe-browser.min.js', + 'node_modules/rn-fetch-blob/polyfill/Blob.js', + 'node_modules/rn-fetch-blob/polyfill/Event.js', + 'node_modules/rn-fetch-blob/polyfill/EventTarget.js', 'node_modules/rn-fetch-blob/polyfill/Fetch.js', + 'node_modules/rn-fetch-blob/polyfill/File.js', + 'node_modules/rn-fetch-blob/polyfill/FileReader.js', + 'node_modules/rn-fetch-blob/polyfill/ProgressEvent.js', + 'node_modules/rn-fetch-blob/polyfill/XMLHttpRequest.js', + 'node_modules/rn-fetch-blob/polyfill/XMLHttpRequestEventTarget.js', + 'node_modules/rn-fetch-blob/polyfill/index.js', + 'node_modules/rn-fetch-blob/utils/log.js', + 'node_modules/rn-fetch-blob/utils/unicode.js', + 'node_modules/rn-fetch-blob/utils/uri.js', + 'node_modules/rn-fetch-blob/utils/uuid.js', 'node_modules/rn-host-detect/index.js', 'node_modules/sc-errors/decycle.js', 'node_modules/sc-errors/index.js', @@ -748,10 +612,8 @@ module.exports = [ 'node_modules/scheduler/index.js', 'node_modules/scheduler/tracing.js', 'node_modules/semver/semver.js', - 'node_modules/shallow-equals/index.js', 'node_modules/stacktrace-parser/dist/stack-trace-parser.umd.js', 'node_modules/symbol-observable/lib/index.js', 'node_modules/symbol-observable/lib/ponyfill.js', - 'node_modules/tinycolor2/tinycolor.js', 'node_modules/url-parse/index.js', ]; \ No newline at end of file diff --git a/packager/modulePaths.js b/packager/modulePaths.js index 6c18ad900..084158aa9 100644 --- a/packager/modulePaths.js +++ b/packager/modulePaths.js @@ -1,87 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -module.exports = ['./node_modules/app/app.js', - './node_modules/app/components/app_icon.js', - './node_modules/app/components/at_mention/at_mention.js', - './node_modules/app/components/at_mention/index.js', - './node_modules/app/components/attachment_button.js', - './node_modules/app/components/badge.js', - './node_modules/app/components/emoji/emoji.js', - './node_modules/app/components/emoji/index.js', - './node_modules/app/components/fade.js', - './node_modules/app/components/file_attachment_list/file_attachment_icon.js', - './node_modules/app/components/file_attachment_list/file_attachment_image.js', - './node_modules/app/components/formatted_date.js', - './node_modules/app/components/formatted_markdown_text.js', - './node_modules/app/components/formatted_text.js', - './node_modules/app/components/formatted_time.js', - './node_modules/app/components/interactive_dialog_controller/index.js', - './node_modules/app/components/interactive_dialog_controller/interactive_dialog_controller.js', - './node_modules/app/components/layout/keyboard_layout/index.js', - './node_modules/app/components/layout/keyboard_layout/keyboard_layout.js', - './node_modules/app/components/loading.js', - './node_modules/app/components/markdown/hashtag/hashtag.js', - './node_modules/app/components/markdown/hashtag/index.js', - './node_modules/app/components/markdown/index.js', - './node_modules/app/components/markdown/markdown.js', - './node_modules/app/components/markdown/markdown_block_quote.js', - './node_modules/app/components/markdown/markdown_code_block/index.js', - './node_modules/app/components/markdown/markdown_code_block/markdown_code_block.js', - './node_modules/app/components/markdown/markdown_emoji/index.js', - './node_modules/app/components/markdown/markdown_emoji/markdown_emoji.js', - './node_modules/app/components/markdown/markdown_image/index.js', - './node_modules/app/components/markdown/markdown_image/markdown_image.js', - './node_modules/app/components/markdown/markdown_link/index.js', - './node_modules/app/components/markdown/markdown_link/markdown_link.js', - './node_modules/app/components/markdown/markdown_list.js', - './node_modules/app/components/markdown/markdown_list_item.js', - './node_modules/app/components/markdown/markdown_table/index.js', - './node_modules/app/components/markdown/markdown_table/markdown_table.js', - './node_modules/app/components/markdown/markdown_table_cell/index.js', - './node_modules/app/components/markdown/markdown_table_cell/markdown_table_cell.js', - './node_modules/app/components/markdown/markdown_table_image/index.js', - './node_modules/app/components/markdown/markdown_table_image/markdown_table_image.js', - './node_modules/app/components/markdown/markdown_table_row/index.js', - './node_modules/app/components/markdown/markdown_table_row/markdown_table_row.js', - './node_modules/app/components/network_indicator/index.js', - './node_modules/app/components/network_indicator/network_indicator.js', + +module.exports = ['./node_modules/app/components/loading.js', './node_modules/app/components/paper_plane.js', - './node_modules/app/components/post/index.js', - './node_modules/app/components/post/post.js', - './node_modules/app/components/post_body/index.js', - './node_modules/app/components/post_body/post_body.js', - './node_modules/app/components/post_header/index.js', - './node_modules/app/components/post_header/post_header.js', - './node_modules/app/components/post_header/post_pre_header.js', - './node_modules/app/components/post_list/date_header/date_header.js', - './node_modules/app/components/post_list/date_header/index.js', - './node_modules/app/components/post_list/index.js', - './node_modules/app/components/post_list/new_messages_divider.js', - './node_modules/app/components/post_list/post_list.js', - './node_modules/app/components/post_list_retry.js', - './node_modules/app/components/post_profile_picture/index.js', - './node_modules/app/components/post_profile_picture/post_profile_picture.js', - './node_modules/app/components/post_textbox/components/typing/index.js', - './node_modules/app/components/post_textbox/components/typing/typing.js', - './node_modules/app/components/post_textbox/index.js', - './node_modules/app/components/post_textbox/post_textbox.android.js', - './node_modules/app/components/post_textbox/post_textbox_base.js', - './node_modules/app/components/profile_picture/index.js', - './node_modules/app/components/profile_picture/profile_picture.js', - './node_modules/app/components/progressive_image/index.js', - './node_modules/app/components/progressive_image/progressive_image.js', - './node_modules/app/components/reply_icon.js', - './node_modules/app/components/safe_area_view/index.js', - './node_modules/app/components/safe_area_view/safe_area_view.android.js', - './node_modules/app/components/send_button.js', - './node_modules/app/components/show_more_button/index.js', - './node_modules/app/components/show_more_button/show_more_button.js', - './node_modules/app/components/start/empty_toolbar.js', - './node_modules/app/components/status_bar/index.js', - './node_modules/app/components/status_bar/status_bar.js', - './node_modules/app/components/user_status/index.js', - './node_modules/app/components/user_status/user_status.js', - './node_modules/app/components/vector_icon.js', './node_modules/app/constants/custom_prop_types.js', './node_modules/app/constants/deep_linking.js', './node_modules/app/constants/device.js', @@ -90,7 +11,10 @@ module.exports = ['./node_modules/app/app.js', './node_modules/app/constants/navigation.js', './node_modules/app/constants/permissions.js', './node_modules/app/constants/view.js', - './node_modules/app/fetch_preconfig.js', + './node_modules/app/init/credentials.js', + './node_modules/app/init/emm_provider.js', + './node_modules/app/init/fetch.js', + './node_modules/app/init/global_event_handler.js', './node_modules/app/initial_state.js', './node_modules/app/mattermost.js', './node_modules/app/mattermost_bucket/index.js', @@ -107,7 +31,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/app/reducers/device/is_tablet.js', './node_modules/app/reducers/device/orientation.js', './node_modules/app/reducers/device/status_bar.js', - './node_modules/app/reducers/device/websocket.js', './node_modules/app/reducers/index.js', './node_modules/app/reducers/navigation/index.js', './node_modules/app/reducers/views/announcement.js', @@ -126,40 +49,16 @@ module.exports = ['./node_modules/app/app.js', './node_modules/app/reducers/views/team.js', './node_modules/app/reducers/views/thread.js', './node_modules/app/reducers/views/user.js', - './node_modules/app/screens/channel/channel.android.js', - './node_modules/app/screens/channel/channel_base.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_drawer_button.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_nav_bar.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_search_button/index.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_title/channel_title.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_title/index.js', - './node_modules/app/screens/channel/channel_nav_bar/index.js', - './node_modules/app/screens/channel/channel_nav_bar/settings_drawer_button.js', - './node_modules/app/screens/channel/channel_post_list/channel_post_list.js', - './node_modules/app/screens/channel/channel_post_list/index.js', - './node_modules/app/screens/channel/index.js', - './node_modules/app/screens/entry/entry.js', - './node_modules/app/screens/entry/index.js', - './node_modules/app/screens/index.js', - './node_modules/app/screens/select_server/index.js', - './node_modules/app/screens/select_server/select_server.js', - './node_modules/app/selectors/client_upgrade.js', + './node_modules/app/store/ephemeral_store.js', './node_modules/app/store/index.js', './node_modules/app/store/middleware.js', './node_modules/app/store/thunk.js', './node_modules/app/telemetry/index.js', './node_modules/app/telemetry/telemetry.android.js', - './node_modules/app/telemetry/telemetry_utils.js', - './node_modules/app/utils/avoid_native_bridge.js', - './node_modules/app/utils/general.js', - './node_modules/app/utils/i18n.js', './node_modules/app/utils/image_cache_manager.js', - './node_modules/app/utils/network.js', './node_modules/app/utils/push_notifications.js', './node_modules/app/utils/sentry/middleware.js', './node_modules/app/utils/theme.js', - './node_modules/app/utils/time_tracker.js', './node_modules/index.js', './node_modules/node_modules/@babel/runtime/helpers/arrayWithHoles.js', './node_modules/node_modules/@babel/runtime/helpers/assertThisInitialized.js', @@ -185,9 +84,7 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/@babel/runtime/regenerator/index.js', './node_modules/node_modules/@react-native-community/async-storage/lib/AsyncStorage.js', './node_modules/node_modules/@react-native-community/async-storage/lib/index.js', - './node_modules/node_modules/@react-native-community/netinfo/js/index.js', './node_modules/node_modules/base-64/base64.js', - './node_modules/node_modules/commonmark-react-renderer/src/commonmark-react-renderer.js', './node_modules/node_modules/component-emitter/index.js', './node_modules/node_modules/core-js/modules/_a-function.js', './node_modules/node_modules/core-js/modules/_add-to-unscopables.js', @@ -265,12 +162,10 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/core-js/modules/es6.set.js', './node_modules/node_modules/core-js/modules/es6.string.includes.js', './node_modules/node_modules/core-js/modules/es6.string.iterator.js', - './node_modules/node_modules/core-js/modules/es6.string.starts-with.js', './node_modules/node_modules/core-js/modules/es6.symbol.js', './node_modules/node_modules/core-js/modules/es7.array.includes.js', './node_modules/node_modules/core-js/modules/es7.object.define-getter.js', './node_modules/node_modules/core-js/modules/es7.object.define-setter.js', - './node_modules/node_modules/core-js/modules/es7.object.entries.js', './node_modules/node_modules/core-js/modules/es7.object.values.js', './node_modules/node_modules/core-js/modules/es7.symbol.async-iterator.js', './node_modules/node_modules/core-js/modules/web.dom.iterable.js', @@ -288,7 +183,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/fbjs/lib/keyOf.js', './node_modules/node_modules/fbjs/lib/performance.js', './node_modules/node_modules/fbjs/lib/performanceNow.js', - './node_modules/node_modules/fuse.js/dist/fuse.js', './node_modules/node_modules/global/window.js', './node_modules/node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js', './node_modules/node_modules/intl-format-cache/index.js', @@ -450,10 +344,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/mattermost-redux/reducers/requests/search.js', './node_modules/node_modules/mattermost-redux/reducers/requests/teams.js', './node_modules/node_modules/mattermost-redux/reducers/requests/users.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/emojis.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/general.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/integrations.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/teams.js', './node_modules/node_modules/mattermost-redux/store/configureStore.prod.js', './node_modules/node_modules/mattermost-redux/store/helpers.js', './node_modules/node_modules/mattermost-redux/store/index.js', @@ -462,16 +352,9 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/mattermost-redux/store/reducer_registry.js', './node_modules/node_modules/mattermost-redux/utils/deep_freeze.js', './node_modules/node_modules/mattermost-redux/utils/event_emitter.js', - './node_modules/node_modules/mattermost-redux/utils/file_utils.js', - './node_modules/node_modules/mattermost-redux/utils/helpers.js', './node_modules/node_modules/mattermost-redux/utils/key_mirror.js', - './node_modules/node_modules/mattermost-redux/utils/post_list.js', './node_modules/node_modules/mattermost-redux/utils/theme_utils.js', - './node_modules/node_modules/moment-timezone/index.js', - './node_modules/node_modules/moment-timezone/moment-timezone.js', - './node_modules/node_modules/moment/moment.js', './node_modules/node_modules/object-assign/index.js', - './node_modules/node_modules/pascalcase/index.js', './node_modules/node_modules/path-to-regexp/index.js', './node_modules/node_modules/path-to-regexp/node_modules/isarray/index.js', './node_modules/node_modules/promise/setimmediate/core.js', @@ -493,11 +376,8 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-intl/locale-data/index.js', './node_modules/node_modules/react-is/cjs/react-is.production.min.js', './node_modules/node_modules/react-is/index.js', - './node_modules/node_modules/react-native-button/Button.js', - './node_modules/node_modules/react-native-button/coalesceNonElementChildren.js', './node_modules/node_modules/react-native-device-info/deviceinfo.js', - './node_modules/node_modules/react-native-linear-gradient/common.js', - './node_modules/node_modules/react-native-linear-gradient/index.android.js', + './node_modules/node_modules/react-native-keychain/index.js', './node_modules/node_modules/react-native-local-auth/LocalAuth.android.js', './node_modules/node_modules/react-native-local-auth/index.js', './node_modules/node_modules/react-native-navigation/lib/dist/Navigation.js', @@ -529,7 +409,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-native/Libraries/Animated/src/AnimatedImplementation.js', './node_modules/node_modules/react-native/Libraries/Animated/src/Easing.js', './node_modules/node_modules/react-native/Libraries/Animated/src/components/AnimatedText.js', - './node_modules/node_modules/react-native/Libraries/Animated/src/components/AnimatedView.js', './node_modules/node_modules/react-native/Libraries/Animated/src/createAnimatedComponent.js', './node_modules/node_modules/react-native/Libraries/Animated/src/nodes/AnimatedInterpolation.js', './node_modules/node_modules/react-native/Libraries/Animated/src/nodes/AnimatedNode.js', @@ -550,13 +429,9 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-native/Libraries/Components/TextInput/TextInputState.js', './node_modules/node_modules/react-native/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js', './node_modules/node_modules/react-native/Libraries/Components/Touchable/Touchable.js', - './node_modules/node_modules/react-native/Libraries/Components/Touchable/TouchableOpacity.js', - './node_modules/node_modules/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js', './node_modules/node_modules/react-native/Libraries/Components/View/PlatformViewPropTypes.android.js', './node_modules/node_modules/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js', './node_modules/node_modules/react-native/Libraries/Components/View/ReactNativeViewAttributes.js', - './node_modules/node_modules/react-native/Libraries/Components/View/View.js', - './node_modules/node_modules/react-native/Libraries/Components/View/ViewNativeComponent.js', './node_modules/node_modules/react-native/Libraries/Core/ExceptionsManager.js', './node_modules/node_modules/react-native/Libraries/Core/InitializeCore.js', './node_modules/node_modules/react-native/Libraries/Core/ReactNativeVersion.js', @@ -593,7 +468,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-native/Libraries/Image/AssetRegistry.js', './node_modules/node_modules/react-native/Libraries/Image/AssetSourceResolver.js', './node_modules/node_modules/react-native/Libraries/Image/Image.android.js', - './node_modules/node_modules/react-native/Libraries/Image/ImageBackground.js', './node_modules/node_modules/react-native/Libraries/Image/resolveAssetSource.js', './node_modules/node_modules/react-native/Libraries/Interaction/PanResponder.js', './node_modules/node_modules/react-native/Libraries/JSInspector/InspectorAgent.js', @@ -602,10 +476,8 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-native/Libraries/Linking/Linking.js', './node_modules/node_modules/react-native/Libraries/Network/NetInfo.js', './node_modules/node_modules/react-native/Libraries/Performance/Systrace.js', - './node_modules/node_modules/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js', './node_modules/node_modules/react-native/Libraries/Promise.js', './node_modules/node_modules/react-native/Libraries/ReactNative/AppRegistry.js', - './node_modules/node_modules/react-native/Libraries/ReactNative/FabricUIManager.js', './node_modules/node_modules/react-native/Libraries/ReactNative/I18nManager.js', './node_modules/node_modules/react-native/Libraries/ReactNative/UIManager.js', './node_modules/node_modules/react-native/Libraries/ReactNative/UIManagerProperties.js', @@ -622,7 +494,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-native/Libraries/StyleSheet/processTransform.js', './node_modules/node_modules/react-native/Libraries/Text/Text.js', './node_modules/node_modules/react-native/Libraries/Text/TextStylePropTypes.js', - './node_modules/node_modules/react-native/Libraries/Utilities/BackHandler.android.js', './node_modules/node_modules/react-native/Libraries/Utilities/DeviceInfo.js', './node_modules/node_modules/react-native/Libraries/Utilities/Dimensions.js', './node_modules/node_modules/react-native/Libraries/Utilities/HMRClient.js', @@ -706,15 +577,30 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/redux-thunk/lib/index.js', './node_modules/node_modules/redux/lib/redux.js', './node_modules/node_modules/regenerator-runtime/runtime.js', - './node_modules/node_modules/reselect/lib/index.js', './node_modules/node_modules/rn-fetch-blob/android.js', './node_modules/node_modules/rn-fetch-blob/cba/index.js', './node_modules/node_modules/rn-fetch-blob/class/RNFetchBlobFile.js', + './node_modules/node_modules/rn-fetch-blob/class/RNFetchBlobReadStream.js', + './node_modules/node_modules/rn-fetch-blob/class/RNFetchBlobSession.js', + './node_modules/node_modules/rn-fetch-blob/class/RNFetchBlobWriteStream.js', './node_modules/node_modules/rn-fetch-blob/fs.js', './node_modules/node_modules/rn-fetch-blob/index.js', './node_modules/node_modules/rn-fetch-blob/json-stream.js', './node_modules/node_modules/rn-fetch-blob/lib/oboe-browser.min.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/Blob.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/Event.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/EventTarget.js', './node_modules/node_modules/rn-fetch-blob/polyfill/Fetch.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/File.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/FileReader.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/ProgressEvent.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/XMLHttpRequest.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/XMLHttpRequestEventTarget.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/index.js', + './node_modules/node_modules/rn-fetch-blob/utils/log.js', + './node_modules/node_modules/rn-fetch-blob/utils/unicode.js', + './node_modules/node_modules/rn-fetch-blob/utils/uri.js', + './node_modules/node_modules/rn-fetch-blob/utils/uuid.js', './node_modules/node_modules/rn-host-detect/index.js', './node_modules/node_modules/sc-errors/decycle.js', './node_modules/node_modules/sc-errors/index.js', @@ -724,9 +610,7 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/scheduler/index.js', './node_modules/node_modules/scheduler/tracing.js', './node_modules/node_modules/semver/semver.js', - './node_modules/node_modules/shallow-equals/index.js', './node_modules/node_modules/stacktrace-parser/dist/stack-trace-parser.umd.js', './node_modules/node_modules/symbol-observable/lib/index.js', './node_modules/node_modules/symbol-observable/lib/ponyfill.js', - './node_modules/node_modules/tinycolor2/tinycolor.js', - './node_modules/node_modules/url-parse/index.js']; \ No newline at end of file + './node_modules/node_modules/url-parse/index.js']; diff --git a/share_extension/android/extension_post/extension_post.js b/share_extension/android/extension_post/extension_post.js index a1cfa5679..00f357d03 100644 --- a/share_extension/android/extension_post/extension_post.js +++ b/share_extension/android/extension_post/extension_post.js @@ -21,7 +21,6 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import Video from 'react-native-video'; import LocalAuth from 'react-native-local-auth'; import RNFetchBlob from 'rn-fetch-blob'; -import {getGenericPassword} from 'react-native-keychain'; import {Client4} from 'mattermost-redux/client'; import {Preferences} from 'mattermost-redux/constants'; @@ -30,8 +29,8 @@ import {getFormattedFileSize, lookupMimeType} from 'mattermost-redux/utils/file_ import Loading from 'app/components/loading'; import PaperPlane from 'app/components/paper_plane'; import {MAX_FILE_COUNT} from 'app/constants/post_textbox'; +import {getCurrentServerUrl, getAppCredentials} from 'app/init/credentials'; import mattermostManaged from 'app/mattermost_managed'; -import avoidNativeBridge from 'app/utils/avoid_native_bridge'; import {getExtensionFromMime} from 'app/utils/file'; import {emptyFunction} from 'app/utils/general'; import {setCSRFFromCookie} from 'app/utils/security'; @@ -49,7 +48,6 @@ import { import ChannelButton from './channel_button'; import TeamButton from './team_button'; -const {Initialization} = NativeModules; const defaultTheme = Preferences.THEMES.default; const extensionSvg = { csv: ExcelSvg, @@ -232,32 +230,18 @@ export default class ExtensionPost extends PureComponent { getAppCredentials = async () => { try { - const credentials = await avoidNativeBridge( - () => { - return Initialization.credentialsExist; - }, - () => { - return Initialization.credentials; - }, - () => { - return getGenericPassword(); - } - ); + const url = await getCurrentServerUrl(); + const credentials = await getAppCredentials(); if (credentials) { - const passwordParsed = credentials.password.split(','); + const token = credentials.password; - // password == token, url - if (passwordParsed.length === 2) { - const [token, url] = passwordParsed; - - if (url && url !== 'undefined' && token && token !== 'undefined') { - this.token = token; - this.url = url; - Client4.setUrl(url); - Client4.setToken(token); - await setCSRFFromCookie(url); - } + if (url && url !== 'undefined' && token && token !== 'undefined') { + this.token = token; + this.url = url; + Client4.setUrl(url); + Client4.setToken(token); + await setCSRFFromCookie(url); } } } catch (error) {