Merge branch 'master' into rnn-v2

This commit is contained in:
Miguel Alatzar 2019-07-02 18:15:19 -07:00
commit 51aac1e6e7
80 changed files with 1859 additions and 2114 deletions

View file

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

View file

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

View file

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

View file

@ -44,7 +44,8 @@
android:exported="false" />
<activity
android:name="com.reactnativenavigation.controllers.NavigationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:resizeableActivity="true"/>
<activity
android:name="com.mattermost.share.ShareActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"

View file

@ -1,146 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.support.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.mattermost.react_native_interface.AsyncStorageHelper;
import com.mattermost.react_native_interface.KeysReadableArray;
import com.mattermost.react_native_interface.ResolvePromise;
import com.oblador.keychain.KeychainModule;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class InitializationModule extends ReactContextBaseJavaModule {
static final String TOOLBAR_BACKGROUND = "TOOLBAR_BACKGROUND";
static final String TOOLBAR_TEXT_COLOR = "TOOLBAR_TEXT_COLOR";
static final String APP_BACKGROUND = "APP_BACKGROUND";
private final Application mApplication;
public InitializationModule(Application application, ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
}
@Override
public String getName() {
return "Initialization";
}
@Nullable
@Override
public Map<String, Object> getConstants() {
Map<String, Object> constants = new HashMap<>();
/**
* Package all native module variables in constants
* in order to avoid the native bridge
*
* KeyStore:
* credentialsExist
* deviceToken
* currentUserId
* token
* url
*
* AsyncStorage:
* toolbarBackground
* toolbarTextColor
* appBackground
*
* Miscellaneous:
* MattermostManaged.Config
* replyFromPushNotification
*/
MainApplication app = (MainApplication) mApplication;
final Boolean[] credentialsExist = {false};
final WritableMap[] credentials = {null};
final Object[] config = {null};
// Get KeyStore credentials
KeychainModule module = new KeychainModule(this.getReactApplicationContext());
module.getGenericPasswordForOptions(null, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
credentialsExist[0] = false;
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
credentialsExist[0] = true;
credentials[0] = map;
}
}
});
// Get managedConfig from MattermostManagedModule
MattermostManagedModule.getInstance().getConfig(new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
WritableNativeMap nativeMap = (WritableNativeMap) value;
config[0] = value;
}
});
// Get AsyncStorage key/values
final ArrayList<String> keys = new ArrayList<String>(5);
keys.add(TOOLBAR_BACKGROUND);
keys.add(TOOLBAR_TEXT_COLOR);
keys.add(APP_BACKGROUND);
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
@Override
public int size() {
return keys.size();
}
@Override
public String getString(int index) {
return keys.get(index);
}
};
AsyncStorageHelper asyncStorage = new AsyncStorageHelper(this.getReactApplicationContext());
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
String toolbarBackground = asyncStorageResults.get(TOOLBAR_BACKGROUND);
String toolbarTextColor = asyncStorageResults.get(TOOLBAR_TEXT_COLOR);
String appBackground = asyncStorageResults.get(APP_BACKGROUND);
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;
}
}

View file

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

View file

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

View file

@ -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<String> keys = new ArrayList<String>(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<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
String serverUrl = asyncStorageResults.get(CURRENT_SERVER_URL);
keychainModule.getGenericPasswordForOptions(serverUrl, promise);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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', () => ({

View file

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

View file

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

View file

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

View file

@ -50,6 +50,7 @@ export default class AttachmentActions extends PureComponent {
<ActionButton
key={action.id}
id={action.id}
cookie={action.cookie}
name={action.name}
postId={postId}
/>

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,149 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PostTextBox should match, full snapshot 1`] = `
<React.Fragment>
<Connect(Typing) />
<View
onLayout={[Function]}
style={
Object {
"alignItems": "flex-end",
"backgroundColor": "#ffffff",
"borderTopColor": "rgba(61,60,64,0.2)",
"borderTopWidth": 1,
"flexDirection": "row",
"paddingVertical": 4,
}
}
>
<AttachmentButton
blurTextBox={[Function]}
browseFileTypes="public.item"
canBrowseFiles={true}
canBrowsePhotoLibrary={true}
canBrowseVideoLibrary={true}
canTakePhoto={true}
canTakeVideo={true}
extraOptions={null}
fileCount={0}
maxFileCount={5}
maxFileSize={1024}
navigator={
Object {
"showModal": [MockFunction],
}
}
onShowFileMaxWarning={[Function]}
onShowFileSizeWarning={[Function]}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
uploadFiles={[Function]}
validMimeTypes={Array []}
/>
<View
style={
Array [
Object {
"alignItems": "stretch",
"backgroundColor": "#fff",
"flex": 1,
"flexDirection": "row",
"marginRight": 10,
},
]
}
>
<TextInput
allowFontScaling={true}
blurOnSubmit={false}
disableFullscreenUI={true}
editable={true}
keyboardType="default"
multiline={true}
onChangeText={[Function]}
onEndEditing={[Function]}
onSelectionChange={[Function]}
placeholder="Write to Test Channel"
placeholderTextColor="rgba(0,0,0,0.5)"
rejectResponderTermination={true}
style={
Object {
"color": "#000",
"flex": 1,
"fontSize": 14,
"maxHeight": 100,
"paddingBottom": 8,
"paddingLeft": 12,
"paddingRight": 12,
"paddingTop": 8,
}
}
underlineColorAndroid="transparent"
value=""
/>
<Fade
visible={false}
>
<SendButton
disabled={false}
handleSendMessage={[Function]}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
</Fade>
</View>
</View>
</React.Fragment>
`;

View file

@ -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(
<PostTextbox {...baseProps}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should emit the event but no text is save to draft', () => {
const wrapper = shallowWithIntl(
<PostTextbox {...baseProps}/>
);
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(
<PostTextbox {...baseProps}/>
);
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);
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

128
app/init/credentials.js Normal file
View file

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

165
app/init/emm_provider.js Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = (
<View>
<StatusBar headerColor={app.toolbarBackground}/>
<EmptyToolbar
theme={toolbarTheme}
isLandscape={isLandscape}
/>
</View>
);
loading = (
<ChannelLoader
backgroundColor={backgroundColor}
channelIsLoading={true}
/>
);
} else {
loading = <Loading/>;
}
return (
<SafeAreaView
navBarBackgroundColor={app.toolbarBackground}
backgroundColor={backgroundColor}
navigator={navigator}
>
{toolbar}
{loading}
</SafeAreaView>
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -40,7 +40,7 @@ export default makeStyleSheetFromTheme((theme) => {
color: theme.centerChannelColor,
flex: 1,
fontSize: 17,
lineHeight: 43,
alignSelf: 'center',
},
arrowContainer: {
justifyContent: 'center',

View file

@ -3,6 +3,9 @@
class EphemeralStore {
constructor() {
this.appStarted = false;
this.appStartedFromPushNotification = false;
this.deviceToken = null;
this.componentIdStack = [];
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = "<group>"; };
7F581D36221ED5C60099E66B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
7F581F77221EEA5A0099E66B /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = "<group>"; };
7F5BA34522B99B7B005B05D3 /* Mattermost+RCTUITextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Mattermost+RCTUITextView.h"; path = "Mattermost/Mattermost+RCTUITextView.h"; sourceTree = "<group>"; };
7F5BA34622B99B7B005B05D3 /* Mattermost+RCTUITextView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "Mattermost+RCTUITextView.m"; path = "Mattermost/Mattermost+RCTUITextView.m"; sourceTree = "<group>"; };
7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNDocumentPicker.xcodeproj; path = "../node_modules/react-native-document-picker/ios/RNDocumentPicker.xcodeproj"; sourceTree = "<group>"; };
7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = "<group>"; };
7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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;

View file

@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.20.0</string>
<string>1.21.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -34,7 +34,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>198</string>
<string>203</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View file

@ -0,0 +1,17 @@
//
// Mattermost+RCTUITextView.h
// Mattermost
//
// Created by Elias Nahum on 6/18/19.
// Copyright © 2019 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Mattermost_RCTUITextView : NSObject
@end
NS_ASSUME_NONNULL_END

View file

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

View file

@ -6,7 +6,6 @@
// See License.txt for license information.
//
#import "RCTUITextView.h"
#import "MattermostManaged.h"
#import <UploadAttachments/MMMConstants.h>
@ -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

View file

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.20.0</string>
<string>1.21.0</string>
<key>CFBundleVersion</key>
<string>198</string>
<string>203</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View file

@ -15,10 +15,10 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.20.0</string>
<string>1.21.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>198</string>
<string>203</string>
</dict>
</plist>

View file

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.20.0</string>
<string>1.21.0</string>
<key>CFBundleVersion</key>
<string>198</string>
<string>203</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View file

@ -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 = "<group>"; };
7F82907022C295AC0035544F /* MMKeychainManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMKeychainManager.m; sourceTree = "<group>"; };
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 = "<group>"; };
7FABE04C2213818900D0F595 /* MattermostBucket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MattermostBucket.m; sourceTree = "<group>"; };
@ -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 = "<group>";
};
7FABE02D22137F2900D0F595 = {
isa = PBXGroup;
children = (
7FABE03822137F2900D0F595 /* UploadAttachments */,
7FABE03722137F2900D0F595 /* Products */,
7F82901A22C28F770035544F /* Frameworks */,
);
sourceTree = "<group>";
};
@ -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",

View file

@ -0,0 +1,3 @@
@interface MMKeychainManager : NSObject
-(NSDictionary *)getInternetCredentialsForServer:(NSString *)server withOptions:(NSDictionary *)options;
@end

View file

@ -0,0 +1,73 @@
#import <Security/Security.h>
#import "MMKeychainManager.h"
#import <LocalAuthentication/LAContext.h>
#import <UIKit/UIKit.h>
@implementation MMKeychainManager
// Messages from the comments in <Security/SecBase.h>
#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

View file

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

View file

@ -1,8 +1,10 @@
#import <Foundation/Foundation.h>
#import "MattermostBucket.h"
#import "MMKeychainManager.h"
@interface StoreManager : NSObject
@property MattermostBucket *bucket;
@property MMKeychainManager *keychain;
@property (nonatomic, strong) NSDictionary *entities;
+(instancetype)shared;

View file

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

View file

@ -5,3 +5,4 @@
#import "MMMConstants.h"
#import "MattermostBucket.h"
#import "StoreManager.h"
#import "MMKeychainManager.h"

View file

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

219
package-lock.json generated
View file

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

View file

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

View file

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

View file

@ -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'];
'./node_modules/node_modules/url-parse/index.js'];

View file

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