diff --git a/Makefile b/Makefile
index 15a234d60..a9e20209f 100644
--- a/Makefile
+++ b/Makefile
@@ -150,7 +150,7 @@ run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
run-android: | check-device-android pre-run prepare-android-build ## Runs the app on an Android emulator or dev device
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
- node ./node_modules/react-native/local-cli/cli.js start & echo echo Running Android app in development; \
+ node ./node_modules/react-native/local-cli/cli.js start & echo Running Android app in development; \
if [ ! -z ${VARIANT} ]; then \
react-native run-android --no-packager --variant=${VARIANT}; \
else \
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ba36bb1a4..5c2258d8a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -56,6 +56,21 @@
+
+
+
+
+
+ // for sharing
+
+
+
diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java
index 60ee8dcac..cf1904b39 100644
--- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java
+++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java
@@ -1,5 +1,6 @@
package com.mattermost.rnbeta;
+import com.mattermost.share.SharePackage;
import android.app.Application;
import android.support.annotation.NonNull;
import android.content.Context;
@@ -70,13 +71,14 @@ public class MainApplication extends NavigationApplication implements INotificat
new ReactNativeExceptionHandlerPackage(),
new ReactNativeYouTube(),
new ReactVideoPackage(),
- new RNReactNativeDocViewerPackage()
+ new RNReactNativeDocViewerPackage(),
+ new SharePackage()
);
}
@Override
public String getJSMainModuleName() {
- return "index";
+ return "index.android";
}
@Override
diff --git a/android/app/src/main/java/com/mattermost/share/RealPathUtil.java b/android/app/src/main/java/com/mattermost/share/RealPathUtil.java
new file mode 100644
index 000000000..0c6ea1387
--- /dev/null
+++ b/android/app/src/main/java/com/mattermost/share/RealPathUtil.java
@@ -0,0 +1,175 @@
+package com.mattermost.share;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.content.ContentUris;
+import android.os.Environment;
+import android.webkit.MimeTypeMap;
+
+import android.os.ParcelFileDescriptor;
+import java.io.*;
+import java.nio.channels.FileChannel;
+
+// Class based on the steveevers DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
+
+public class RealPathUtil {
+ public static String getRealPathFromURI(final Context context, final Uri uri) {
+
+ final boolean isKitKatOrNewer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+
+ // DocumentProvider
+ if (isKitKatOrNewer && DocumentsContract.isDocumentUri(context, uri)) {
+ // ExternalStorageProvider
+ if (isExternalStorageDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ if ("primary".equalsIgnoreCase(type)) {
+ return Environment.getExternalStorageDirectory() + "/" + split[1];
+ }
+ } else if (isDownloadsDocument(uri)) {
+ // DownloadsProvider
+
+ final String id = DocumentsContract.getDocumentId(uri);
+ final Uri contentUri = ContentUris.withAppendedId(
+ Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+
+ return getDataColumn(context, contentUri, null, null);
+ } else if (isMediaDocument(uri)) {
+ // MediaProvider
+
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ Uri contentUri = null;
+ if ("image".equals(type)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if ("video".equals(type)) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else if ("audio".equals(type)) {
+ contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ final String selection = "_id=?";
+ final String[] selectionArgs = new String[] {
+ split[1]
+ };
+
+ return getDataColumn(context, contentUri, selection, selectionArgs);
+ }
+ } else if ("content".equalsIgnoreCase(uri.getScheme())) {
+ // MediaStore (and general)
+
+ if (isGooglePhotosUri(uri)) {
+ return uri.getLastPathSegment();
+ }
+
+ String path = getDataColumn(context, uri, null, null);
+
+ if (path != null) {
+ return path;
+ }
+
+ // Try save to tmp file, and return tmp file path
+ return getPathFromSavingTempFile(context, uri);
+ } else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ return uri.getPath();
+ }
+
+ return null;
+ }
+
+ public static String getPathFromSavingTempFile(Context context, final Uri uri) {
+ File tmpFile;
+ try {
+ String fileName = uri.getLastPathSegment();
+ tmpFile = File.createTempFile("tmp", fileName, context.getCacheDir());
+
+ ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
+
+ FileChannel src = new FileInputStream(pfd.getFileDescriptor()).getChannel();
+ FileChannel dst = new FileOutputStream(tmpFile).getChannel();
+ dst.transferFrom(src, 0, src.size());
+ src.close();
+ dst.close();
+ } catch (IOException ex) {
+ return null;
+ }
+ return tmpFile.getAbsolutePath();
+ }
+
+ public static String getDataColumn(Context context, Uri uri, String selection,
+ String[] selectionArgs) {
+
+ Cursor cursor = null;
+ final String column = "_data";
+ final String[] projection = {
+ column
+ };
+
+ try {
+ cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
+ null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final int index = cursor.getColumnIndexOrThrow(column);
+ return cursor.getString(index);
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ return null;
+ }
+
+
+ public static boolean isExternalStorageDocument(Uri uri) {
+ return "com.android.externalstorage.documents".equals(uri.getAuthority());
+ }
+
+ public static boolean isDownloadsDocument(Uri uri) {
+ return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+ }
+
+ public static boolean isMediaDocument(Uri uri) {
+ return "com.android.providers.media.documents".equals(uri.getAuthority());
+ }
+
+ public static boolean isGooglePhotosUri(Uri uri) {
+ return "com.google.android.apps.photos.content".equals(uri.getAuthority());
+ }
+
+ public static String getExtension(String uri) {
+ if (uri == null) {
+ return null;
+ }
+
+ int dot = uri.lastIndexOf(".");
+ if (dot >= 0) {
+ return uri.substring(dot);
+ } else {
+ // No extension.
+ return "";
+ }
+ }
+
+ public static String getMimeType(File file) {
+
+ String extension = getExtension(file.getName());
+
+ if (extension.length() > 0)
+ return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1));
+
+ return "application/octet-stream";
+ }
+
+ public static String getMimeType(String filePath) {
+ File file = new File(filePath);
+ return getMimeType(file);
+ }
+}
diff --git a/android/app/src/main/java/com/mattermost/share/ShareActivity.java b/android/app/src/main/java/com/mattermost/share/ShareActivity.java
new file mode 100644
index 000000000..b15a9ad97
--- /dev/null
+++ b/android/app/src/main/java/com/mattermost/share/ShareActivity.java
@@ -0,0 +1,13 @@
+package com.mattermost.share;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.facebook.react.ReactActivity;
+
+public class ShareActivity extends ReactActivity {
+ @Override
+ protected String getMainComponentName() {
+ return "MattermostShare";
+ }
+}
diff --git a/android/app/src/main/java/com/mattermost/share/ShareModule.java b/android/app/src/main/java/com/mattermost/share/ShareModule.java
new file mode 100644
index 000000000..f92c2efdc
--- /dev/null
+++ b/android/app/src/main/java/com/mattermost/share/ShareModule.java
@@ -0,0 +1,199 @@
+package com.mattermost.share;
+
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.bridge.Arguments;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+import android.graphics.Bitmap;
+import java.io.InputStream;
+import java.io.File;
+import java.util.ArrayList;
+import javax.annotation.Nonnull;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONException;
+
+import java.io.IOException;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.MultipartBody;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class ShareModule extends ReactContextBaseJavaModule {
+ private final OkHttpClient client = new OkHttpClient();
+ public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+ public ShareModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public String getName() {
+ return "MattermostShare";
+ }
+
+ @ReactMethod
+ public void clear() {
+ Activity currentActivity = getCurrentActivity();
+
+ if (currentActivity != null) {
+ Intent intent = currentActivity.getIntent();
+ intent.setAction("");
+ intent.removeExtra(Intent.EXTRA_TEXT);
+ intent.removeExtra(Intent.EXTRA_STREAM);
+ }
+ }
+
+ @ReactMethod
+ public void close(ReadableMap data) {
+ getCurrentActivity().finish();
+
+ if (data != null) {
+ ReadableArray files = data.getArray("files");
+ String serverUrl = data.getString("url");
+ String token = data.getString("token");
+ JSONObject postData = buildPostObject(data);
+
+ if (files.size() > 0) {
+ uploadFiles(serverUrl, token, files, postData);
+ } else {
+ try {
+ post(serverUrl, token, postData);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ @ReactMethod
+ public void data(Promise promise) {
+ promise.resolve(processIntent());
+ }
+
+ public WritableArray processIntent() {
+ WritableMap map = Arguments.createMap();
+ WritableArray items = Arguments.createArray();
+
+ String text = "";
+ String type = "";
+ String action = "";
+
+ Activity currentActivity = getCurrentActivity();
+
+ if (currentActivity != null) {
+ Intent intent = currentActivity.getIntent();
+ action = intent.getAction();
+ type = intent.getType();
+ if (type == null) {
+ type = "";
+ }
+
+ if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
+ text = intent.getStringExtra(Intent.EXTRA_TEXT);
+ map.putString("value", text);
+ map.putString("type", type);
+ items.pushMap(map);
+ } else if (Intent.ACTION_SEND.equals(action)) {
+ Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
+ map.putString("value", text);
+ map.putString("type", type);
+ items.pushMap(map);
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ ArrayList uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ for (Uri uri : uris) {
+ String filePath = RealPathUtil.getRealPathFromURI(currentActivity, uri);
+ map = Arguments.createMap();
+ text = "file://" + filePath;
+ map.putString("value", text);
+ map.putString("type", RealPathUtil.getMimeType(filePath));
+ items.pushMap(map);
+ }
+ }
+ }
+
+ return items;
+ }
+
+ private JSONObject buildPostObject(ReadableMap data) {
+ JSONObject json = new JSONObject();
+ try {
+ json.put("user_id", data.getString("currentUserId"));
+ json.put("channel_id", data.getString("channelId"));
+ json.put("message", data.getString("value"));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return json;
+ }
+
+ private void post(String serverUrl, String token, JSONObject postData) throws IOException {
+ RequestBody body = RequestBody.create(JSON, postData.toString());
+ Request request = new Request.Builder()
+ .header("Authorization", "BEARER " + token)
+ .url(serverUrl + "/api/v4/posts")
+ .post(body)
+ .build();
+ Response response = client.newCall(request).execute();
+ }
+
+ private void uploadFiles(String serverUrl, String token, ReadableArray files, JSONObject postData) {
+ try {
+ MultipartBody.Builder builder = new MultipartBody.Builder()
+ .setType(MultipartBody.FORM);
+
+ for(int i = 0 ; i < files.size() ; i++) {
+ ReadableMap file = files.getMap(i);
+ String filePath = file.getString("fullPath").replaceFirst("file://", "");
+ File fileInfo = new File(filePath);
+ if (fileInfo.exists()) {
+ final MediaType MEDIA_TYPE = MediaType.parse(file.getString("mimeType"));
+ builder.addFormDataPart("files", file.getString("filename"), RequestBody.create(MEDIA_TYPE, fileInfo));
+ }
+ }
+
+ builder.addFormDataPart("channel_id", postData.getString("channel_id"));
+ RequestBody body = builder.build();
+ Request request = new Request.Builder()
+ .header("Authorization", "BEARER " + token)
+ .url(serverUrl + "/api/v4/files")
+ .post(body)
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ if (response.isSuccessful()) {
+ String responseData = response.body().string();
+ JSONObject responseJson = new JSONObject(responseData);
+ JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
+ JSONArray file_ids = new JSONArray();
+ for(int i = 0 ; i < fileInfoArray.length() ; i++) {
+ JSONObject fileInfo = fileInfoArray.getJSONObject(i);
+ file_ids.put(fileInfo.getString("id"));
+ }
+ postData.put("file_ids", file_ids);
+ post(serverUrl, token, postData);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/mattermost/share/SharePackage.java b/android/app/src/main/java/com/mattermost/share/SharePackage.java
new file mode 100644
index 000000000..a1c745f96
--- /dev/null
+++ b/android/app/src/main/java/com/mattermost/share/SharePackage.java
@@ -0,0 +1,27 @@
+package com.mattermost.share;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.uimanager.ViewManager;
+import com.facebook.react.ReactPackage;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class SharePackage implements ReactPackage {
+ @Override
+ public List createNativeModules(ReactApplicationContext reactContext) {
+ return Arrays.asList(new ShareModule(reactContext));
+ }
+
+ public List> createJSModules() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+}
diff --git a/app/constants/view.js b/app/constants/view.js
index ff315f64d..6c74e8b80 100644
--- a/app/constants/view.js
+++ b/app/constants/view.js
@@ -59,7 +59,8 @@ const ViewTypes = keyMirror({
SET_LAST_UPGRADE_CHECK: null,
- ADD_RECENT_EMOJI: null
+ ADD_RECENT_EMOJI: null,
+ EXTENSION_SELECTED_TEAM_ID: null
});
export default {
diff --git a/app/mattermost.js b/app/mattermost.js
index 48b9c3dda..d7e894fae 100644
--- a/app/mattermost.js
+++ b/app/mattermost.js
@@ -415,14 +415,10 @@ export default class Mattermost {
if (Platform.OS === 'android') {
// In case of Android we need to handle the bridge being initialized by HeadlessJS
Promise.resolve(Navigation.isAppLaunched()).then((appLaunched) => {
- if (notification) {
- if (appLaunched) {
- this.launchApp(); // App is launched -> show UI
- } else {
- new NativeEventsReceiver().appLaunched(this.launchApp); // App hasn't been launched yet -> show the UI only when needed.
- }
+ if (appLaunched) {
+ this.launchApp(); // App is launched -> show UI
} else {
- this.launchApp();
+ new NativeEventsReceiver().appLaunched(this.launchApp); // App hasn't been launched yet -> show the UI only when needed.
}
});
} else if (isNotActive) {
diff --git a/app/reducers/views/extension.js b/app/reducers/views/extension.js
new file mode 100644
index 000000000..09bca2b64
--- /dev/null
+++ b/app/reducers/views/extension.js
@@ -0,0 +1,19 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+import {combineReducers} from 'redux';
+
+import {ViewTypes} from 'app/constants';
+
+function selectedTeamId(state = '', action) {
+ switch (action.type) {
+ case ViewTypes.EXTENSION_SELECTED_TEAM_ID: {
+ return action.data;
+ }
+ default:
+ return state;
+ }
+}
+
+export default combineReducers({
+ selectedTeamId
+});
diff --git a/app/reducers/views/index.js b/app/reducers/views/index.js
index bd0572d0e..7dcea57aa 100644
--- a/app/reducers/views/index.js
+++ b/app/reducers/views/index.js
@@ -5,6 +5,7 @@ import {combineReducers} from 'redux';
import channel from './channel';
import clientUpgrade from './client_upgrade';
+import extension from './extension';
import fetchCache from './fetch_cache';
import i18n from './i18n';
import login from './login';
@@ -18,6 +19,7 @@ import thread from './thread';
export default combineReducers({
channel,
clientUpgrade,
+ extension,
fetchCache,
i18n,
login,
diff --git a/app/store/index.js b/app/store/index.js
index 000864a68..01476c841 100644
--- a/app/store/index.js
+++ b/app/store/index.js
@@ -43,7 +43,7 @@ const setTransforms = [
export default function configureAppStore(initialState) {
const viewsBlackListFilter = createBlacklistFilter(
'views',
- ['login', 'root']
+ ['extension', 'login', 'root']
);
const typingBlackListFilter = createBlacklistFilter(
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index c45d4b590..78d13bc2a 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -2007,6 +2007,10 @@
"mobile.error_handler.button": "Relaunch",
"mobile.error_handler.description": "\nClick relaunch to open the app again. After restart, you can report the problem from the settings menu.\n",
"mobile.error_handler.title": "Unexpected error occurred",
+ "mobile.extension.authentication_required": "Authentication required: Please first login using the app.",
+ "mobile.extension.permission": "Mattermost needs access to the device storage to share files.",
+ "mobile.extension.file_limit": "Sharing is limited to a maximum of 5 files.",
+ "mobile.extension.title": "Share in Mattermost",
"mobile.file_upload.camera": "Take Photo or Video",
"mobile.file_upload.library": "Photo Library",
"mobile.file_upload.max_warning": "Uploads limited to 5 files maximum.",
@@ -2114,6 +2118,7 @@
"mobile.routes.mfa": "Multi-factor Authentication",
"mobile.routes.postsList": "Posts List",
"mobile.routes.saml": "Single SignOn",
+ "mobile.routes.selectChannel": "Select Channel",
"mobile.routes.selectTeam": "Select Team",
"mobile.routes.settings": "Settings",
"mobile.routes.sso": "Single Sign-On",
diff --git a/index.android.js b/index.android.js
new file mode 100644
index 000000000..3973260de
--- /dev/null
+++ b/index.android.js
@@ -0,0 +1,11 @@
+// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {AppRegistry} from 'react-native';
+
+/* eslint-disable no-unused-vars */
+import Mattermost from 'app/mattermost';
+import ShareExtension from 'share_extension/android';
+
+AppRegistry.registerComponent('MattermostShare', () => ShareExtension);
+const app = new Mattermost();
diff --git a/package.json b/package.json
index 61e777b99..9fb42c0fa 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"react-native-vector-icons": "4.4.2",
"react-native-video": "2.0.0",
"react-native-youtube": "1.0.1",
+ "react-navigation": "1.0.0-beta.23",
"react-redux": "5.0.6",
"redux": "3.7.2",
"redux-batched-actions": "0.2.0",
diff --git a/share_extension/android/actions/index.js b/share_extension/android/actions/index.js
new file mode 100644
index 000000000..665a9fdcb
--- /dev/null
+++ b/share_extension/android/actions/index.js
@@ -0,0 +1,29 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {fetchMyChannelsAndMembers} from 'mattermost-redux/actions/channels';
+
+import {ViewTypes} from 'app/constants';
+import {getDefaultChannelForTeam} from 'share_extension/android/selectors';
+
+export function getTeamChannels(teamId) {
+ return async (dispatch, getState) => {
+ let defaultChannel = getDefaultChannelForTeam(getState(), teamId);
+
+ if (!defaultChannel) {
+ await fetchMyChannelsAndMembers(teamId)(dispatch, getState);
+ defaultChannel = getDefaultChannelForTeam(getState(), teamId);
+ }
+
+ return defaultChannel.id;
+ };
+}
+
+export function extensionSelectTeamId(teamId) {
+ return async (dispatch, getState) => {
+ dispatch({
+ type: ViewTypes.EXTENSION_SELECTED_TEAM_ID,
+ data: teamId
+ }, getState);
+ };
+}
diff --git a/share_extension/android/extension_channels/extension_channels.js b/share_extension/android/extension_channels/extension_channels.js
new file mode 100644
index 000000000..0b42319f0
--- /dev/null
+++ b/share_extension/android/extension_channels/extension_channels.js
@@ -0,0 +1,269 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {intlShape} from 'react-intl';
+import {NavigationActions} from 'react-navigation';
+import {
+ ActivityIndicator,
+ SectionList,
+ Text,
+ View
+} from 'react-native';
+
+import {Preferences} from 'mattermost-redux/constants';
+
+import SearchBar from 'app/components/search_bar';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+import ExtensionChannelItem from 'share_extension/common/extension_channel_item';
+
+const defaultTheme = Preferences.THEMES.default;
+
+export default class ExtensionTeam extends PureComponent {
+ static propTypes = {
+ directChannels: PropTypes.array,
+ navigation: PropTypes.object.isRequired,
+ privateChannels: PropTypes.array,
+ publicChannels: PropTypes.array
+ };
+
+ static defaultProps = {
+ directChannels: [],
+ privateChannels: [],
+ publicChannels: []
+ };
+
+ static contextTypes = {
+ intl: intlShape
+ };
+
+ static navigationOptions = ({navigation}) => ({
+ title: navigation.state.params.title
+ });
+
+ state = {
+ sections: null
+ };
+
+ componentWillMount() {
+ this.buildSections();
+ }
+
+ buildSections = (term) => {
+ const sections = [];
+ let {
+ directChannels: directFiltered,
+ privateChannels: privateFiletered,
+ publicChannels: publicFiltered
+ } = this.props;
+
+ if (term) {
+ directFiltered = directFiltered.filter((c) => c.display_name.toLowerCase().includes(term.toLowerCase()));
+ privateFiletered = privateFiletered.filter((c) => c.display_name.toLowerCase().includes(term.toLowerCase()));
+ publicFiltered = publicFiltered.filter((c) => c.display_name.toLowerCase().includes(term.toLowerCase()));
+ }
+
+ if (publicFiltered.length) {
+ sections.push({
+ id: 'sidebar.channels',
+ defaultMessage: 'PUBLIC CHANNELS',
+ data: publicFiltered
+ });
+ }
+
+ if (privateFiletered.length) {
+ sections.push({
+ id: 'sidebar.pg',
+ defaultMessage: 'PRIVATE CHANNELS',
+ data: privateFiletered
+ });
+ }
+
+ if (directFiltered.length) {
+ sections.push({
+ id: 'sidebar.direct',
+ defaultMessage: 'DIRECT MESSAGES',
+ data: directFiltered
+ });
+ }
+
+ this.setState({sections});
+ };
+
+ handleSelectChannel = async (channel) => {
+ const {state} = this.props.navigation;
+ const backAction = NavigationActions.back();
+
+ if (state.params && state.params.onSelectChannel) {
+ state.params.onSelectChannel(channel.id);
+ }
+
+ this.props.navigation.dispatch(backAction);
+ };
+
+ handleSearch = (term) => {
+ this.setState({term}, () => {
+ if (this.throttleTimeout) {
+ clearTimeout(this.throttleTimeout);
+ }
+
+ this.throttleTimeout = setTimeout(() => {
+ this.buildSections(term);
+ }, 300);
+ });
+ };
+
+ keyExtractor = (item) => item.id;
+
+ renderBody = (styles) => {
+ const {sections} = this.state;
+
+ if (!sections) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+ };
+
+ renderItem = ({item}) => {
+ const {navigation} = this.props;
+ const {params = {}} = navigation.state;
+ const {currentChannelId} = params;
+
+ return (
+
+ );
+ };
+
+ renderItemSeparator = () => {
+ const styles = getStyleSheet(defaultTheme);
+
+ return (
+
+ );
+ };
+
+ renderSearchBar = (styles) => {
+ const {formatMessage} = this.context.intl;
+
+ return (
+
+
+
+ );
+ };
+
+ renderSectionHeader = ({section}) => {
+ const {intl} = this.context;
+ const styles = getStyleSheet(defaultTheme);
+ const {
+ defaultMessage,
+ id
+ } = section;
+
+ return (
+
+
+
+ {intl.formatMessage({id, defaultMessage}).toUpperCase()}
+
+
+
+ );
+ };
+
+ render() {
+ const styles = getStyleSheet(defaultTheme);
+
+ return (
+
+ {this.renderBody(styles)}
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ flex: {
+ flex: 1
+ },
+ separator: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
+ height: 1
+ },
+ loadingContainer: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center'
+ },
+ searchContainer: {
+ paddingBottom: 2
+ },
+ searchBarInput: {
+ backgroundColor: '#fff',
+ color: theme.centerChannelColor,
+ fontSize: 15
+ },
+ titleContainer: {
+ height: 30
+ },
+ title: {
+ color: theme.centerChannelColor,
+ fontSize: 15,
+ height: 30,
+ textAlignVertical: 'center',
+ paddingHorizontal: 15
+ },
+ errorContainer: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ paddingHorizontal: 15
+ },
+ error: {
+ color: theme.errorTextColor,
+ fontSize: 14
+ }
+ };
+});
diff --git a/share_extension/android/extension_channels/index.js b/share_extension/android/extension_channels/index.js
new file mode 100644
index 000000000..461fd00b0
--- /dev/null
+++ b/share_extension/android/extension_channels/index.js
@@ -0,0 +1,22 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {
+ getExtensionSortedDirectChannels,
+ getExtensionSortedPrivateChannels,
+ getExtensionSortedPublicChannels
+} from 'share_extension/android/selectors';
+
+import ExtensionChannels from './extension_channels';
+
+function mapStateToProps(state) {
+ return {
+ publicChannels: getExtensionSortedPublicChannels(state),
+ privateChannels: getExtensionSortedPrivateChannels(state),
+ directChannels: getExtensionSortedDirectChannels(state)
+ };
+}
+
+export default connect(mapStateToProps)(ExtensionChannels);
diff --git a/share_extension/android/extension_post/channel_button/channel_button.js b/share_extension/android/extension_post/channel_button/channel_button.js
new file mode 100644
index 000000000..9203d271a
--- /dev/null
+++ b/share_extension/android/extension_post/channel_button/channel_button.js
@@ -0,0 +1,82 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {intlShape} from 'react-intl';
+import {
+ Text,
+ TouchableHighlight,
+ View
+} from 'react-native';
+
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+export default class ChannelButton extends PureComponent {
+ static propTypes = {
+ channel: PropTypes.object.isRequired,
+ theme: PropTypes.object.isRequired,
+ onPress: PropTypes.func.isRequired
+ };
+
+ static contextTypes = {
+ intl: intlShape
+ };
+
+ render() {
+ const {formatMessage} = this.context.intl;
+ const {onPress, channel, theme} = this.props;
+ const channelName = channel ? channel.display_name : '';
+ const styles = getStyleSheet(theme);
+
+ return (
+
+
+
+ {formatMessage({id: 'mobile.share_extension.channel', defaultMessage: 'Channel'})}
+
+
+ {channelName}
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ flex: {
+ flex: 1
+ },
+ buttonContainer: {
+ borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
+ borderBottomWidth: 1,
+ borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
+ borderTopWidth: 1,
+ height: 70,
+ paddingHorizontal: 15
+ },
+ buttonWrapper: {
+ alignItems: 'flex-start',
+ flex: 1
+ },
+ buttonLabel: {
+ fontSize: 16,
+ marginTop: 16,
+ marginBottom: 3
+ },
+ buttonValue: {
+ color: changeOpacity(theme.centerChannelColor, 0.6),
+ fontSize: 14
+ }
+ };
+});
diff --git a/share_extension/android/extension_post/channel_button/index.js b/share_extension/android/extension_post/channel_button/index.js
new file mode 100644
index 000000000..ddb2c980c
--- /dev/null
+++ b/share_extension/android/extension_post/channel_button/index.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getChannel} from 'mattermost-redux/selectors/entities/channels';
+
+import ChannelButton from './channel_button';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ channel: getChannel(state, ownProps.channelId)
+ };
+}
+
+export default connect(mapStateToProps)(ChannelButton);
diff --git a/share_extension/android/extension_post/extension_post.js b/share_extension/android/extension_post/extension_post.js
new file mode 100644
index 000000000..f9eb7ab77
--- /dev/null
+++ b/share_extension/android/extension_post/extension_post.js
@@ -0,0 +1,608 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import {NavigationActions} from 'react-navigation';
+import TouchableItem from 'react-navigation/src/views/TouchableItem';
+import PropTypes from 'prop-types';
+import {intlShape} from 'react-intl';
+import {
+ Image,
+ NativeModules,
+ PermissionsAndroid,
+ ScrollView,
+ Text,
+ TextInput,
+ View
+} from 'react-native';
+import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
+import Video from 'react-native-video';
+import LocalAuth from 'react-native-local-auth';
+
+import {Preferences} from 'mattermost-redux/constants';
+import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
+
+import PaperPlane from 'app/components/paper_plane';
+import mattermostManaged from 'app/mattermost_managed';
+import {emptyFunction} from 'app/utils/general';
+import {wrapWithPreventDoubleTap} from 'app/utils/tap';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+import {
+ ExcelSvg,
+ GenericSvg,
+ PdfSvg,
+ PptSvg,
+ ZipSvg
+} from 'share_extension/common/icons';
+
+import ChannelButton from './channel_button';
+import TeamButton from './team_button';
+
+const defalultTheme = Preferences.THEMES.default;
+const extensionSvg = {
+ csv: ExcelSvg,
+ pdf: PdfSvg,
+ ppt: PptSvg,
+ pptx: PptSvg,
+ xls: ExcelSvg,
+ xlsx: ExcelSvg,
+ zip: ZipSvg
+};
+const ShareExtension = NativeModules.MattermostShare;
+const INPUT_HEIGHT = 150;
+const MAX_MESSAGE_LENGTH = 4000;
+
+export default class ExtensionPost extends PureComponent {
+ static propTypes = {
+ channelId: PropTypes.string.isRequired,
+ currentUserId: PropTypes.string.isRequired,
+ navigation: PropTypes.object.isRequired,
+ teamId: PropTypes.string.isRequired,
+ token: PropTypes.string,
+ url: PropTypes.string
+ };
+
+ static contextTypes = {
+ intl: intlShape
+ };
+
+ static navigationOptions = ({navigation}) => {
+ const {params = {}} = navigation.state;
+ const title = params.title || '';
+ const headerLeft = (
+
+
+
+
+
+ );
+ const headerRight = (
+
+
+
+
+
+ );
+
+ return {headerLeft, headerRight, title};
+ };
+
+ constructor(props, context) {
+ super(props, context);
+
+ props.navigation.setParams({
+ title: context.intl.formatMessage({
+ id: 'mobile.extension.title',
+ defaultMessage: 'Share in Mattermost'
+ })
+ });
+
+ this.state = {
+ channelId: props.channelId,
+ files: [],
+ hasPermission: null,
+ teamId: props.teamId,
+ value: ''
+ };
+ }
+
+ componentDidMount() {
+ this.props.navigation.setParams({
+ close: this.onClose,
+ post: this.onPost
+ });
+ this.auth();
+ }
+
+ auth = async () => {
+ try {
+ const {formatMessage} = this.context.intl;
+ const config = await mattermostManaged.getConfig();
+
+ if (config) {
+ const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
+ const vendor = config.vendor || 'Mattermost';
+ if (authNeeded) {
+ const isSecured = await mattermostManaged.isDeviceSecure();
+ if (isSecured) {
+ try {
+ await LocalAuth.auth({
+ reason: formatMessage({
+ id: 'mobile.managed.secured_by',
+ defaultMessage: 'Secured by {vendor}'
+ }, {vendor}),
+ fallbackToPasscode: true,
+ suppressEnterPassword: false
+ });
+ } catch (err) {
+ return this.onClose();
+ }
+ }
+ }
+ }
+ } catch (e) {
+ // do nothing
+ }
+
+ return this.initialize();
+ };
+
+ getInputRef = (ref) => {
+ this.input = ref;
+ };
+
+ goToChannels = wrapWithPreventDoubleTap(() => {
+ const {formatMessage} = this.context.intl;
+ const {navigation} = this.props;
+ const navigateAction = NavigationActions.navigate({
+ routeName: 'Channels',
+ params: {
+ title: formatMessage({
+ id: 'mobile.routes.selectChannel',
+ defaultMessage: 'Select Channel'
+ }),
+ currentChannelId: this.state.channelId,
+ onSelectChannel: this.handleSelectChannel
+ }
+ });
+ navigation.dispatch(navigateAction);
+ });
+
+ goToTeams = wrapWithPreventDoubleTap(() => {
+ const {formatMessage} = this.context.intl;
+ const {navigation} = this.props;
+ const navigateAction = NavigationActions.navigate({
+ routeName: 'Teams',
+ params: {
+ title: formatMessage({
+ id: 'mobile.routes.selectTeam',
+ defaultMessage: 'Select Team'
+ }),
+ currentTeamId: this.state.teamId,
+ onSelectTeam: this.handleSelectTeam
+ }
+ });
+ navigation.dispatch(navigateAction);
+ });
+
+ handleBlur = () => {
+ if (this.input) {
+ this.input.setNativeProps({
+ autoScroll: false
+ });
+ }
+ };
+
+ handleFocus = () => {
+ if (this.input) {
+ this.input.setNativeProps({
+ autoScroll: true
+ });
+ }
+ };
+
+ handleSelectChannel = (channelId) => {
+ this.setState({channelId});
+ };
+
+ handleSelectTeam = (teamId, defaultChannelId) => {
+ this.setState({teamId, channelId: defaultChannelId});
+ };
+
+ handleTextChange = (value) => {
+ this.setState({value});
+ };
+
+ initialize = async () => {
+ const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE);
+ let granted;
+ if (!hasPermission) {
+ granted = await PermissionsAndroid.request(
+ PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE
+ );
+ }
+
+ if (hasPermission || granted === PermissionsAndroid.RESULTS.GRANTED) {
+ const data = await ShareExtension.data();
+ return this.loadData(data);
+ }
+
+ return this.setState({hasPermission: false});
+ };
+
+ loadData = async (items) => {
+ const {token, url} = this.props;
+ if (token && url) {
+ const text = [];
+ const files = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ switch (item.type) {
+ case 'text/plain':
+ text.push(item.value);
+ break;
+ default: {
+ const fullPath = item.value;
+ const filename = fullPath.replace(/^.*[\\/]/, '');
+ const extension = filename.split('.').pop();
+ files.push({
+ extension,
+ filename,
+ fullPath,
+ mimeType: lookupMimeType(filename.toLowerCase()),
+ type: item.type
+ });
+ break;
+ }
+ }
+ }
+
+ const value = text.join('\n');
+
+ this.setState({files, value, hasPermission: true});
+ }
+ };
+
+ onClose = (data) => {
+ ShareExtension.close(data.nativeEvent ? null : data);
+ };
+
+ onPost = () => {
+ const {channelId, files, value} = this.state;
+ const {currentUserId, token, url} = this.props;
+
+ const data = {
+ channelId,
+ currentUserId,
+ files,
+ token,
+ url,
+ value
+ };
+
+ this.onClose(data);
+ };
+
+ renderBody = () => {
+ const {formatMessage} = this.context.intl;
+ const {value} = this.state;
+
+ return (
+
+
+ {this.renderFiles()}
+
+ );
+ };
+
+ renderChannelButton = () => {
+ const {channelId} = this.state;
+
+ return (
+
+ );
+ };
+
+ renderFiles = () => {
+ const {files} = this.state;
+
+ return files.map((file, index) => {
+ let component;
+
+ if (file.type.startsWith('image')) {
+ component = (
+
+
+
+ );
+ } else if (file.type.startsWith('video')) {
+ component = (
+
+
+ );
+ } else {
+ let SvgIcon = extensionSvg[file.extension];
+ if (!SvgIcon) {
+ SvgIcon = GenericSvg;
+ }
+
+ component = (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {component}
+
+ {file.filename}
+
+
+ );
+ });
+ };
+
+ renderTeamButton = () => {
+ const {teamId} = this.state;
+
+ return (
+
+ );
+ };
+
+ render() {
+ const {formatMessage} = this.context.intl;
+ const {token, url} = this.props;
+ const {hasPermission, files} = this.state;
+
+ if (hasPermission === false) {
+ return (
+
+
+
+ {formatMessage({
+ id: 'mobile.extension.permission',
+ defaultMessage: 'Mattermost needs access to the device storage to share files.'
+ })}
+
+
+
+ );
+ } else if (files.length > 5) {
+ return (
+
+
+
+ {formatMessage({
+ id: 'mobile.extension.file_limit',
+ defaultMessage: 'Sharing is limited to a maximum of 5 files.'
+ })}
+
+
+
+ );
+ } else if (token && url) {
+ return (
+
+
+ {this.renderBody()}
+
+ {this.renderTeamButton()}
+ {this.renderChannelButton()}
+
+
+
+ );
+ }
+
+ return (
+
+
+ {formatMessage({
+ id: 'mobile.extension.authentication_required',
+ defaultMessage: 'Authentication required: Please first login using the app.'
+ })}
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ left: {
+ alignItems: 'center',
+ height: 50,
+ justifyContent: 'center',
+ width: 50
+ },
+ closeButton: {
+ color: theme.sidebarHeaderTextColor,
+ fontSize: 20
+ },
+ flex: {
+ flex: 1
+ },
+ container: {
+ flex: 1,
+ backgroundColor: theme.centerChannelBg
+ },
+ wrapper: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
+ flex: 1
+ },
+ scrollView: {
+ padding: 15
+ },
+ input: {
+ color: theme.centerChannelColor,
+ fontSize: 17,
+ height: INPUT_HEIGHT,
+ marginBottom: 5,
+ textAlignVertical: 'top',
+ width: '100%'
+ },
+ unauthenticatedContainer: {
+ alignItems: 'center',
+ flex: 1,
+ paddingHorizontal: 20,
+ paddingTop: 35
+ },
+ unauthenticated: {
+ color: theme.errorTextColor,
+ fontSize: 14
+ },
+ fileContainer: {
+ alignItems: 'center',
+ backgroundColor: theme.centerChannelBg,
+ borderColor: changeOpacity(theme.centerChannelColor, 0.2),
+ borderRadius: 4,
+ borderWidth: 1,
+ flexDirection: 'row',
+ height: 48,
+ marginBottom: 10,
+ width: '100%'
+ },
+ filename: {
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ fontSize: 13,
+ flex: 1
+ },
+ otherContainer: {
+ borderBottomLeftRadius: 4,
+ borderTopLeftRadius: 4,
+ height: 48,
+ marginRight: 10,
+ paddingVertical: 10,
+ width: 38
+ },
+ otherWrapper: {
+ borderRightWidth: 1,
+ borderRightColor: changeOpacity(theme.centerChannelColor, 0.2),
+ flex: 1
+ },
+ fileIcon: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ flex: 1
+ },
+ imageContainer: {
+ justifyContent: 'center',
+ borderBottomLeftRadius: 4,
+ borderTopLeftRadius: 4,
+ height: 48,
+ marginRight: 10,
+ width: 38,
+ overflow: 'hidden'
+ },
+ image: {
+ alignItems: 'center',
+ borderBottomLeftRadius: 4,
+ borderTopLeftRadius: 4,
+ height: 46,
+ justifyContent: 'center',
+ overflow: 'hidden',
+ width: 38
+ },
+ video: {
+ backgroundColor: theme.centerChannelBg,
+ alignItems: 'center',
+ height: 48,
+ justifyContent: 'center',
+ overflow: 'hidden',
+ width: 38
+ }
+ };
+});
+
+const styles = getStyleSheet(defalultTheme);
diff --git a/share_extension/android/extension_post/index.js b/share_extension/android/extension_post/index.js
new file mode 100644
index 000000000..8cf2c4252
--- /dev/null
+++ b/share_extension/android/extension_post/index.js
@@ -0,0 +1,24 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
+import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
+import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
+
+import ExtensionPost from './extension_post';
+
+function mapStateToProps(state) {
+ const {token, url} = state.entities.general.credentials;
+
+ return {
+ channelId: getCurrentChannelId(state),
+ currentUserId: getCurrentUserId(state),
+ teamId: getCurrentTeamId(state),
+ token,
+ url
+ };
+}
+
+export default connect(mapStateToProps)(ExtensionPost);
diff --git a/share_extension/android/extension_post/team_button/index.js b/share_extension/android/extension_post/team_button/index.js
new file mode 100644
index 000000000..cbb68c4ca
--- /dev/null
+++ b/share_extension/android/extension_post/team_button/index.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTeam} from 'mattermost-redux/selectors/entities/teams';
+
+import TeamButton from './team_button';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ team: getTeam(state, ownProps.teamId)
+ };
+}
+
+export default connect(mapStateToProps)(TeamButton);
diff --git a/share_extension/android/extension_post/team_button/team_button.js b/share_extension/android/extension_post/team_button/team_button.js
new file mode 100644
index 000000000..7fdb27dfe
--- /dev/null
+++ b/share_extension/android/extension_post/team_button/team_button.js
@@ -0,0 +1,76 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {intlShape} from 'react-intl';
+import {
+ Text,
+ TouchableHighlight,
+ View
+} from 'react-native';
+
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+export default class TeamButton extends PureComponent {
+ static propTypes = {
+ team: PropTypes.object.isRequired,
+ theme: PropTypes.object.isRequired,
+ onPress: PropTypes.func.isRequired
+ };
+
+ static contextTypes = {
+ intl: intlShape
+ };
+
+ render() {
+ const {formatMessage} = this.context.intl;
+ const {onPress, team, theme} = this.props;
+ const teamName = team ? team.display_name : '';
+ const styles = getStyleSheet(theme);
+
+ return (
+
+
+
+ {formatMessage({id: 'mobile.share_extension.team', defaultMessage: 'Team'})}
+
+
+ {teamName}
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ flex: {
+ flex: 1
+ },
+ buttonContainer: {
+ borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
+ borderTopWidth: 1,
+ height: 70,
+ paddingHorizontal: 15
+ },
+ buttonWrapper: {
+ alignItems: 'flex-start',
+ flex: 1
+ },
+ buttonLabel: {
+ fontSize: 16,
+ marginTop: 16,
+ marginBottom: 3
+ },
+ buttonValue: {
+ color: changeOpacity(theme.centerChannelColor, 0.6),
+ fontSize: 14
+ }
+ };
+});
diff --git a/share_extension/android/extension_teams/extension_teams.js b/share_extension/android/extension_teams/extension_teams.js
new file mode 100644
index 000000000..46b265cbc
--- /dev/null
+++ b/share_extension/android/extension_teams/extension_teams.js
@@ -0,0 +1,126 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {intlShape} from 'react-intl';
+import {NavigationActions} from 'react-navigation';
+import {
+ ActivityIndicator,
+ FlatList,
+ View
+} from 'react-native';
+
+import {Preferences} from 'mattermost-redux/constants';
+
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+import TeamItem from './team_item';
+
+const defaultTheme = Preferences.THEMES.default;
+
+export default class ExtensionTeam extends PureComponent {
+ static propTypes = {
+ actions: PropTypes.shape({
+ extensionSelectTeamId: PropTypes.func.isRequired,
+ getTeamChannels: PropTypes.func.isRequired
+ }).isRequired,
+ navigation: PropTypes.object.isRequired,
+ teamIds: PropTypes.array
+ };
+
+ static defaultProps = {
+ teamIds: []
+ };
+
+ static contextTypes = {
+ intl: intlShape
+ };
+
+ static navigationOptions = ({navigation}) => ({
+ title: navigation.state.params.title
+ });
+
+ state = {
+ loading: false
+ };
+
+ handleSelectTeam = async (teamId) => {
+ const {state} = this.props.navigation;
+ const backAction = NavigationActions.back();
+
+ if (state.params && state.params.onSelectTeam) {
+ this.setState({loading: true});
+ const channelId = await this.props.actions.getTeamChannels(teamId);
+ this.props.actions.extensionSelectTeamId(teamId);
+ state.params.onSelectTeam(teamId, channelId);
+ }
+
+ this.props.navigation.dispatch(backAction);
+ };
+
+ keyExtractor = (item) => item;
+
+ renderItemSeparator = () => {
+ const styles = getStyleSheet(defaultTheme);
+
+ return (
+
+ );
+ };
+
+ renderItem = ({item}) => {
+ const {navigation} = this.props;
+ const {params} = navigation.state;
+
+ return (
+
+ );
+ };
+
+ render() {
+ const styles = getStyleSheet(defaultTheme);
+
+ if (this.state.loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ loadingContainer: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center'
+ },
+ separator: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
+ height: 1
+ }
+ };
+});
diff --git a/share_extension/android/extension_teams/index.js b/share_extension/android/extension_teams/index.js
new file mode 100644
index 000000000..576a6acee
--- /dev/null
+++ b/share_extension/android/extension_teams/index.js
@@ -0,0 +1,28 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+
+import {getMySortedTeamIds} from 'mattermost-redux/selectors/entities/teams';
+
+import {extensionSelectTeamId, getTeamChannels} from 'share_extension/android/actions';
+
+import ExtensionTeams from './extension_teams';
+
+function mapStateToProps(state) {
+ return {
+ teamIds: getMySortedTeamIds(state)
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ extensionSelectTeamId,
+ getTeamChannels
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ExtensionTeams);
diff --git a/share_extension/android/extension_teams/team_item/index.js b/share_extension/android/extension_teams/team_item/index.js
new file mode 100644
index 000000000..6963717b6
--- /dev/null
+++ b/share_extension/android/extension_teams/team_item/index.js
@@ -0,0 +1,13 @@
+import {connect} from 'react-redux';
+
+import {getTeam} from 'mattermost-redux/selectors/entities/teams';
+
+import TeamItem from './team_item';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ team: getTeam(state, ownProps.teamId)
+ };
+}
+
+export default connect(mapStateToProps)(TeamItem);
diff --git a/share_extension/android/extension_teams/team_item/team_item.js b/share_extension/android/extension_teams/team_item/team_item.js
new file mode 100644
index 000000000..022de34eb
--- /dev/null
+++ b/share_extension/android/extension_teams/team_item/team_item.js
@@ -0,0 +1,123 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {
+ Text,
+ TouchableHighlight,
+ View
+} from 'react-native';
+import IonIcon from 'react-native-vector-icons/Ionicons';
+
+import {wrapWithPreventDoubleTap} from 'app/utils/tap';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+export default class TeamItem extends PureComponent {
+ static propTypes = {
+ currentTeamId: PropTypes.string.isRequired,
+ onSelectTeam: PropTypes.func.isRequired,
+ team: PropTypes.object.isRequired,
+ theme: PropTypes.object.isRequired
+ };
+
+ onPress = wrapWithPreventDoubleTap(() => {
+ const {onSelectTeam, team} = this.props;
+ onSelectTeam(team.id);
+ });
+
+ render() {
+ const {
+ currentTeamId,
+ team,
+ theme
+ } = this.props;
+ const styles = getStyleSheet(theme);
+
+ let current;
+ if (team.id === currentTeamId) {
+ current = (
+
+
+
+ );
+ }
+
+ const icon = (
+
+
+ {team.display_name.substr(0, 2).toUpperCase()}
+
+
+ );
+
+ return (
+
+
+
+ {icon}
+
+ {team.display_name}
+
+ {current}
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ flex: 1,
+ flexDirection: 'row',
+ height: 45,
+ paddingHorizontal: 15
+ },
+ item: {
+ alignItems: 'center',
+ height: 45,
+ flex: 1,
+ flexDirection: 'row'
+ },
+ text: {
+ color: theme.centerChannelColor,
+ flex: 1,
+ fontSize: 16,
+ paddingRight: 5
+ },
+ iconContainer: {
+ alignItems: 'center',
+ backgroundColor: theme.linkColor,
+ borderRadius: 2,
+ height: 30,
+ justifyContent: 'center',
+ width: 30,
+ marginRight: 10
+ },
+ icon: {
+ color: theme.sidebarText,
+ fontFamily: 'OpenSans',
+ fontSize: 15,
+ fontWeight: '600'
+ },
+ checkmarkContainer: {
+ alignItems: 'flex-end'
+ },
+ checkmark: {
+ color: theme.linkColor,
+ fontSize: 16
+ }
+ };
+});
diff --git a/share_extension/android/index.js b/share_extension/android/index.js
new file mode 100644
index 000000000..61e67db4d
--- /dev/null
+++ b/share_extension/android/index.js
@@ -0,0 +1,62 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import {Provider} from 'react-redux';
+import {IntlProvider} from 'react-intl';
+
+import {Client4} from 'mattermost-redux/client';
+
+import {getTranslations} from 'app/i18n';
+import initialState from 'app/initial_state';
+import {getCurrentLocale} from 'app/selectors/i18n';
+import configureStore from 'app/store';
+
+import {extensionSelectTeamId} from './actions';
+import Navigation from './navigation';
+
+export default class ShareApp extends PureComponent {
+ constructor() {
+ super();
+
+ this.store = configureStore(initialState);
+ this.unsubscribeFromStore = this.store.subscribe(this.listenForHydration);
+ this.state = {init: false};
+ }
+
+ listenForHydration = () => {
+ const {dispatch, getState} = this.store;
+ const state = getState();
+ if (state.views.root.hydrationComplete) {
+ const {credentials} = state.entities.general;
+ const {currentTeamId} = state.entities.teams;
+
+ this.unsubscribeFromStore();
+ if (credentials.token && credentials.url) {
+ Client4.setToken(credentials.token);
+ Client4.setUrl(credentials.url);
+ }
+ extensionSelectTeamId(currentTeamId)(dispatch, getState);
+ this.setState({init: true});
+ }
+ };
+
+ render() {
+ if (!this.state.init) {
+ return null;
+ }
+
+ const locale = getCurrentLocale(this.store.getState());
+
+ return (
+
+
+
+
+
+ );
+ }
+}
diff --git a/share_extension/android/navigation.js b/share_extension/android/navigation.js
new file mode 100644
index 000000000..d3fc523b9
--- /dev/null
+++ b/share_extension/android/navigation.js
@@ -0,0 +1,41 @@
+// // Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// // See License.txt for license information.
+
+import {StackNavigator as stackNavigator} from 'react-navigation';
+
+import {Preferences} from 'mattermost-redux/constants';
+
+import ExtensionChannels from './extension_channels';
+import ExtensionPost from './extension_post';
+import ExtensionTeams from './extension_teams';
+
+const theme = Preferences.THEMES.default;
+const Navigation = stackNavigator({
+ Post: {
+ screen: ExtensionPost
+ },
+ Teams: {
+ screen: ExtensionTeams
+ },
+ Channels: {
+ screen: ExtensionChannels
+ }
+}, {
+ navigationOptions: {
+ headerStyle: {
+ backgroundColor: theme.sidebarHeaderBg
+ },
+ headerTitleStyle: {
+ marginHorizontal: 0,
+ left: 0,
+ color: theme.sidebarHeaderTextColor
+ },
+ headerBackTitleStyle: {
+ color: theme.sidebarHeaderTextColor,
+ margin: 0
+ },
+ headerTintColor: theme.sidebarHeaderTextColor
+ }
+});
+
+export default Navigation;
diff --git a/share_extension/android/selectors/index.js b/share_extension/android/selectors/index.js
new file mode 100644
index 000000000..a3e100f25
--- /dev/null
+++ b/share_extension/android/selectors/index.js
@@ -0,0 +1,164 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {createSelector} from 'reselect';
+
+import {General} from 'mattermost-redux/constants';
+import {getAllChannels, getChannelsInTeam, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
+import {getCurrentUser, getUsers} from 'mattermost-redux/selectors/entities/users';
+import {getConfig} from 'mattermost-redux/selectors/entities/general';
+import {getLastPostPerChannel} from 'mattermost-redux/selectors/entities/posts';
+import {
+ getMyPreferences,
+ getTeammateNameDisplaySetting,
+ getVisibleTeammate,
+ getVisibleGroupIds
+} from 'mattermost-redux/selectors/entities/preferences';
+
+import {
+ completeDirectChannelDisplayName,
+ getDirectChannelName,
+ getGroupDisplayNameFromUserIds,
+ getUserIdFromChannelName,
+ isAutoClosed,
+ sortChannelsByDisplayName
+} from 'mattermost-redux/utils/channel_utils';
+import {createIdsSelector} from 'mattermost-redux/utils/helpers';
+
+export const getDefaultChannelForTeam = createSelector(
+ getAllChannels,
+ (state, teamId) => teamId,
+ (channels, teamId) => {
+ return Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
+ }
+);
+
+export const getChannelIdsForExtensionTeam = createIdsSelector(
+ (state) => state.views.extension.selectedTeamId,
+ getChannelsInTeam,
+ (teamId, channelsInTeam) => {
+ return Array.from(channelsInTeam[teamId] || []);
+ }
+);
+
+export const getExtensionSortedPublicChannels = createSelector(
+ getCurrentUser,
+ getAllChannels,
+ getMyChannelMemberships,
+ getChannelIdsForExtensionTeam,
+ (currentUser, channels, myMembers, teamChannelIds) => {
+ if (!currentUser) {
+ return [];
+ }
+
+ const locale = currentUser.locale || 'en';
+ const publicChannels = teamChannelIds.filter((id) => {
+ if (!myMembers[id]) {
+ return false;
+ }
+ const channel = channels[id];
+ return teamChannelIds.includes(id) && channel.type === General.OPEN_CHANNEL;
+ }).map((id) => channels[id]).sort(sortChannelsByDisplayName.bind(null, locale));
+ return publicChannels;
+ }
+);
+
+export const getExtensionSortedPrivateChannels = createSelector(
+ getCurrentUser,
+ getAllChannels,
+ getMyChannelMemberships,
+ getChannelIdsForExtensionTeam,
+ (currentUser, channels, myMembers, teamChannelIds) => {
+ if (!currentUser) {
+ return [];
+ }
+
+ const locale = currentUser.locale || 'en';
+ const publicChannels = teamChannelIds.filter((id) => {
+ if (!myMembers[id]) {
+ return false;
+ }
+ const channel = channels[id];
+ return teamChannelIds.includes(id) && channel.type === General.PRIVATE_CHANNEL;
+ }).map((id) => channels[id]).sort(sortChannelsByDisplayName.bind(null, locale));
+ return publicChannels;
+ }
+);
+
+export const getExtensionSortedDirectChannels = createSelector(
+ getCurrentUser,
+ getUsers,
+ (state) => state.entities.users.profilesInChannel,
+ getAllChannels,
+ getVisibleTeammate,
+ getVisibleGroupIds,
+ getTeammateNameDisplaySetting,
+ getConfig,
+ getMyPreferences,
+ getLastPostPerChannel,
+ (currentUser, profiles, profilesInChannel, channels, teammates, groupIds, settings, config, preferences, lastPosts) => {
+ if (!currentUser) {
+ return [];
+ }
+
+ const locale = currentUser.locale || 'en';
+ const channelValues = Object.values(channels);
+ const directChannelsIds = [];
+ teammates.reduce((result, teammateId) => {
+ const name = getDirectChannelName(currentUser.id, teammateId);
+ const channel = channelValues.find((c) => c.name === name); //eslint-disable-line max-nested-callbacks
+ if (channel) {
+ const lastPost = lastPosts[channel.id];
+ const otherUser = profiles[getUserIdFromChannelName(currentUser.id, channel.name)];
+ if (!isAutoClosed(config, preferences, channel, lastPost ? lastPost.create_at : 0, otherUser ? otherUser.delete_at : 0)) {
+ result.push(channel.id);
+ }
+ }
+ return result;
+ }, directChannelsIds);
+ const directChannels = groupIds.filter((id) => {
+ const channel = channels[id];
+ if (channel) {
+ const lastPost = lastPosts[channel.id];
+ return !isAutoClosed(config, preferences, channels[id], lastPost ? lastPost.create_at : 0);
+ }
+
+ return false;
+ }).concat(directChannelsIds).map((id) => {
+ const channel = channels[id];
+ if (channel.type === General.GM_CHANNEL) {
+ return completeDirectGroupInfo(currentUser.id, profiles, profilesInChannel, settings, channel);
+ }
+ return completeDirectChannelDisplayName(currentUser.id, profiles, settings, channel);
+ }).sort(sortChannelsByDisplayName.bind(null, locale));
+ return directChannels;
+ }
+);
+
+function completeDirectGroupInfo(currentUserId, profiles, profilesInChannel, teammateNameDisplay, channel) {
+ const profilesIds = profilesInChannel[channel.id];
+ const gm = {...channel};
+
+ if (profilesIds) {
+ return Object.assign(gm, {
+ display_name: getGroupDisplayNameFromUserIds(profilesIds, profiles, currentUserId, teammateNameDisplay)
+ });
+ }
+
+ const usernames = gm.display_name.split(', ');
+ const users = Object.values(profiles);
+ const userIds = [];
+ usernames.forEach((username) => {
+ const u = users.find((p) => p.username === username);
+ if (u) {
+ userIds.push(u.id);
+ }
+ });
+ if (usernames.length === userIds.length) {
+ return Object.assign(gm, {
+ display_name: getGroupDisplayNameFromUserIds(userIds, profiles, currentUserId, teammateNameDisplay)
+ });
+ }
+
+ return channel;
+}
diff --git a/share_extension/ios/extension_channel_item.js b/share_extension/common/extension_channel_item.js
similarity index 96%
rename from share_extension/ios/extension_channel_item.js
rename to share_extension/common/extension_channel_item.js
index 8d3a4fef4..2f628de46 100644
--- a/share_extension/ios/extension_channel_item.js
+++ b/share_extension/common/extension_channel_item.js
@@ -13,7 +13,7 @@ import IonIcon from 'react-native-vector-icons/Ionicons';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
-import {DirectChannel, GroupChannel, PublicChannel, PrivateChannel} from 'share_extension/icons/channel_type';
+import {DirectChannel, GroupChannel, PublicChannel, PrivateChannel} from 'share_extension/common/icons/channel_type';
const channelTypes = {
D: DirectChannel,
@@ -103,8 +103,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.centerChannelColor,
flex: 1,
fontSize: 16,
- fontWeight: '600',
- lineHeight: 16,
paddingRight: 5
},
iconContainer: {
diff --git a/share_extension/icons/channel_type.js b/share_extension/common/icons/channel_type.js
similarity index 100%
rename from share_extension/icons/channel_type.js
rename to share_extension/common/icons/channel_type.js
diff --git a/share_extension/icons/excel.js b/share_extension/common/icons/excel.js
similarity index 100%
rename from share_extension/icons/excel.js
rename to share_extension/common/icons/excel.js
diff --git a/share_extension/icons/generic.js b/share_extension/common/icons/generic.js
similarity index 100%
rename from share_extension/icons/generic.js
rename to share_extension/common/icons/generic.js
diff --git a/share_extension/common/icons/index.js b/share_extension/common/icons/index.js
new file mode 100644
index 000000000..701c25e61
--- /dev/null
+++ b/share_extension/common/icons/index.js
@@ -0,0 +1,14 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Excel from './excel';
+import Generic from './generic';
+import Pdf from './pdf';
+import Ppt from './ppt';
+import Zip from './zip';
+
+export const ExcelSvg = Excel;
+export const GenericSvg = Generic;
+export const PdfSvg = Pdf;
+export const PptSvg = Ppt;
+export const ZipSvg = Zip;
diff --git a/share_extension/icons/pdf.js b/share_extension/common/icons/pdf.js
similarity index 100%
rename from share_extension/icons/pdf.js
rename to share_extension/common/icons/pdf.js
diff --git a/share_extension/icons/ppt.js b/share_extension/common/icons/ppt.js
similarity index 100%
rename from share_extension/icons/ppt.js
rename to share_extension/common/icons/ppt.js
diff --git a/share_extension/icons/zip.js b/share_extension/common/icons/zip.js
similarity index 100%
rename from share_extension/icons/zip.js
rename to share_extension/common/icons/zip.js
diff --git a/share_extension/ios/extension_channels.js b/share_extension/ios/extension_channels.js
index dca7b0891..13ad5fb94 100644
--- a/share_extension/ios/extension_channels.js
+++ b/share_extension/ios/extension_channels.js
@@ -20,7 +20,8 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils';
import SearchBar from 'app/components/search_bar';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
-import ExtensionChannelItem from './extension_channel_item';
+import ExtensionChannelItem from 'share_extension/common/extension_channel_item';
+
import ExtensionNavBar from './extension_nav_bar';
export default class ExtensionChannels extends PureComponent {
diff --git a/share_extension/ios/extension_post.js b/share_extension/ios/extension_post.js
index 9a3cbe214..2ce26f2b0 100644
--- a/share_extension/ios/extension_post.js
+++ b/share_extension/ios/extension_post.js
@@ -27,11 +27,13 @@ import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import Config from 'assets/config';
-import ExcelSvg from 'share_extension/icons/excel';
-import GenericSvg from 'share_extension/icons/generic';
-import PdfSvg from 'share_extension/icons/pdf';
-import PptSvg from 'share_extension/icons/ppt';
-import ZipSvg from 'share_extension/icons/zip';
+import {
+ ExcelSvg,
+ GenericSvg,
+ PdfSvg,
+ PptSvg,
+ ZipSvg
+} from 'share_extension/common/icons';
import ExtensionChannels from './extension_channels';
import ExtensionNavBar from './extension_nav_bar';
@@ -278,7 +280,10 @@ export default class ExtensionPost extends PureComponent {
return (
- {'Authentication required: Please first login using the app.'}
+ {formatMessage({
+ id: 'mobile.extension.authentication_required',
+ defaultMessage: 'Authentication required: Please first login using the app.'
+ })}
);
diff --git a/yarn.lock b/yarn.lock
index dc427aa99..ea5540f0d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -696,6 +696,13 @@ babel-plugin-transform-decorators@^6.24.1:
babel-template "^6.24.1"
babel-types "^6.24.1"
+babel-plugin-transform-define@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-define/-/babel-plugin-transform-define-1.3.0.tgz#94c5f9459c810c738cc7c50cbd44a31829d6f319"
+ dependencies:
+ lodash "4.17.4"
+ traverse "0.6.6"
+
babel-plugin-transform-do-expressions@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz#28ccaf92812d949c2cd1281f690c8fdc468ae9bb"
@@ -1589,6 +1596,10 @@ circular-json@^0.3.1:
version "0.3.3"
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+clamp@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/clamp/-/clamp-1.0.1.tgz#66a0e64011816e37196828fdc8c8c147312c8634"
+
cli-cursor@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
@@ -3075,7 +3086,7 @@ hoek@4.x.x:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
-hoist-non-react-statics@^2.2.1:
+hoist-non-react-statics@^2.2.0, hoist-non-react-statics@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
@@ -3926,7 +3937,7 @@ lodash.unset@^4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed"
-lodash@4.x.x, lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@^4.8.0:
+lodash@4.17.4, lodash@4.x.x, lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@^4.8.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -5051,10 +5062,26 @@ react-native-device-info@0.13.0:
version "0.13.0"
resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-0.13.0.tgz#81051eebd3db6959e4f1d2cb0ae8df74105040cf"
+react-native-dismiss-keyboard@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/react-native-dismiss-keyboard/-/react-native-dismiss-keyboard-1.0.0.tgz#32886242b3f2317e121f3aeb9b0a585e2b879b49"
+
react-native-doc-viewer@2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/react-native-doc-viewer/-/react-native-doc-viewer-2.6.8.tgz#448b160efa2c20bfee82c06173d4e05c86c3b87b"
+react-native-drawer-layout-polyfill@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/react-native-drawer-layout-polyfill/-/react-native-drawer-layout-polyfill-1.3.2.tgz#192c84d7a5a6b8a6d2be2c7daa5e4164518d0cc7"
+ dependencies:
+ react-native-drawer-layout "1.3.2"
+
+react-native-drawer-layout@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-1.3.2.tgz#b9740d7663a1dc4f88a61b9c6d93d2d948ea426e"
+ dependencies:
+ react-native-dismiss-keyboard "1.0.0"
+
react-native-drawer@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/react-native-drawer/-/react-native-drawer-2.5.0.tgz#022cba5c0516126a9fc9cce3185cb46c644b51c4"
@@ -5102,7 +5129,7 @@ react-native-linear-gradient@2.4.0:
react-native-local-auth@enahum/react-native-local-auth.git:
version "1.5.0"
- resolved "https://codeload.github.com/enahum/react-native-local-auth/tar.gz/8307d58bba4f713b6b2a60afb9d6366fcef797d7"
+ resolved "https://codeload.github.com/enahum/react-native-local-auth/tar.gz/0d6a27a0471ea6db82fac7c65d713dcc9222feba"
react-native-mock@0.2.7:
version "0.2.7"
@@ -5200,6 +5227,12 @@ react-native-svg@6.0.1-rc.0:
github-download "^0.5.0"
lodash "^4.16.6"
+react-native-tab-view@^0.0.74:
+ version "0.0.74"
+ resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-0.0.74.tgz#62c0c882d9232b461ce181d440d683b4f99d1bd8"
+ dependencies:
+ prop-types "^15.6.0"
+
react-native-tooltip@enahum/react-native-tooltip:
version "5.1.1"
resolved "https://codeload.github.com/enahum/react-native-tooltip/tar.gz/eb47f93a728813b58c01f23ec84f0b8b1bd70bd0"
@@ -5283,6 +5316,18 @@ react-native@0.51.0:
xmldoc "^0.4.0"
yargs "^9.0.0"
+react-navigation@1.0.0-beta.23:
+ version "1.0.0-beta.23"
+ resolved "https://registry.yarnpkg.com/react-navigation/-/react-navigation-1.0.0-beta.23.tgz#e666c4985e80d1ba39662838b78ad681c07cc187"
+ dependencies:
+ babel-plugin-transform-define "^1.3.0"
+ clamp "^1.0.1"
+ hoist-non-react-statics "^2.2.0"
+ path-to-regexp "^1.7.0"
+ prop-types "^15.5.10"
+ react-native-drawer-layout-polyfill "^1.3.2"
+ react-native-tab-view "^0.0.74"
+
react-proxy@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/react-proxy/-/react-proxy-1.1.8.tgz#9dbfd9d927528c3aa9f444e4558c37830ab8c26a"
@@ -6505,7 +6550,7 @@ tr46@^1.0.0:
dependencies:
punycode "^2.1.0"
-traverse@^0.6.6:
+traverse@0.6.6, traverse@^0.6.6:
version "0.6.6"
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"