Android share extension (#1352)
* Android share extension * Feedback review * Feedback review
This commit is contained in:
parent
542f0998b1
commit
926ffe96c5
42 changed files with 2254 additions and 26 deletions
2
Makefile
2
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 \
|
||||
|
|
|
|||
|
|
@ -56,6 +56,21 @@
|
|||
<activity
|
||||
android:name="com.reactnativenavigation.controllers.NavigationActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
|
||||
<activity
|
||||
android:noHistory="false"
|
||||
android:name="com.mattermost.share.ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
// for sharing
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
175
android/app/src/main/java/com/mattermost/share/RealPathUtil.java
Normal file
175
android/app/src/main/java/com/mattermost/share/RealPathUtil.java
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
199
android/app/src/main/java/com/mattermost/share/ShareModule.java
Normal file
199
android/app/src/main/java/com/mattermost/share/ShareModule.java
Normal file
|
|
@ -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<Uri> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(new ShareModule(reactContext));
|
||||
}
|
||||
|
||||
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
19
app/reducers/views/extension.js
Normal file
19
app/reducers/views/extension.js
Normal file
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const setTransforms = [
|
|||
export default function configureAppStore(initialState) {
|
||||
const viewsBlackListFilter = createBlacklistFilter(
|
||||
'views',
|
||||
['login', 'root']
|
||||
['extension', 'login', 'root']
|
||||
);
|
||||
|
||||
const typingBlackListFilter = createBlacklistFilter(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
11
index.android.js
Normal file
11
index.android.js
Normal file
|
|
@ -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();
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
29
share_extension/android/actions/index.js
Normal file
29
share_extension/android/actions/index.js
Normal file
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
269
share_extension/android/extension_channels/extension_channels.js
Normal file
269
share_extension/android/extension_channels/extension_channels.js
Normal file
|
|
@ -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 (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionList
|
||||
sections={sections}
|
||||
ListHeaderComponent={this.renderSearchBar(styles)}
|
||||
ItemSeparatorComponent={this.renderItemSeparator}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
keyExtractor={this.keyExtractor}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='on-drag'
|
||||
initialNumToRender={10}
|
||||
maxToRenderPerBatch={10}
|
||||
stickySectionHeadersEnabled={true}
|
||||
scrollEventThrottle={100}
|
||||
windowSize={5}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = ({item}) => {
|
||||
const {navigation} = this.props;
|
||||
const {params = {}} = navigation.state;
|
||||
const {currentChannelId} = params;
|
||||
|
||||
return (
|
||||
<ExtensionChannelItem
|
||||
channel={item}
|
||||
currentChannelId={currentChannelId}
|
||||
onSelectChannel={this.handleSelectChannel}
|
||||
theme={defaultTheme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderItemSeparator = () => {
|
||||
const styles = getStyleSheet(defaultTheme);
|
||||
|
||||
return (
|
||||
<View style={styles.separator}/>
|
||||
);
|
||||
};
|
||||
|
||||
renderSearchBar = (styles) => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
return (
|
||||
<View style={styles.searchContainer}>
|
||||
<SearchBar
|
||||
ref='search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={43}
|
||||
inputStyle={styles.searchBarInput}
|
||||
placeholderTextColor={changeOpacity(defaultTheme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(defaultTheme.centerChannelColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(defaultTheme.centerChannelColor, 0.3)}
|
||||
titleCancelColor={defaultTheme.centerChannelColor}
|
||||
onChangeText={this.handleSearch}
|
||||
value={this.state.term}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const {intl} = this.context;
|
||||
const styles = getStyleSheet(defaultTheme);
|
||||
const {
|
||||
defaultMessage,
|
||||
id
|
||||
} = section;
|
||||
|
||||
return (
|
||||
<View style={[styles.titleContainer, {backgroundColor: defaultTheme.centerChannelColor}]}>
|
||||
<View style={{backgroundColor: changeOpacity(defaultTheme.centerChannelBg, 0.6), justifyContent: 'center'}}>
|
||||
<Text style={styles.title}>
|
||||
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const styles = getStyleSheet(defaultTheme);
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
{this.renderBody(styles)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
});
|
||||
22
share_extension/android/extension_channels/index.js
Normal file
22
share_extension/android/extension_channels/index.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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 (
|
||||
<TouchableHighlight
|
||||
onPress={onPress}
|
||||
style={styles.buttonContainer}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.2)}
|
||||
>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Text style={styles.buttonLabel}>
|
||||
{formatMessage({id: 'mobile.share_extension.channel', defaultMessage: 'Channel'})}
|
||||
</Text>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.buttonValue}
|
||||
>
|
||||
{channelName}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -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);
|
||||
608
share_extension/android/extension_post/extension_post.js
Normal file
608
share_extension/android/extension_post/extension_post.js
Normal file
|
|
@ -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 = (
|
||||
<TouchableItem
|
||||
accessibilityComponentType='button'
|
||||
accessibilityTraits='button'
|
||||
borderless={true}
|
||||
delayPressIn={0}
|
||||
pressColorAndroid='rgba(0, 0, 0, .32)'
|
||||
onPress={params.close ? params.close : emptyFunction}
|
||||
>
|
||||
<View style={styles.left}>
|
||||
<MaterialIcon
|
||||
name='close'
|
||||
style={styles.closeButton}
|
||||
/>
|
||||
</View>
|
||||
</TouchableItem>
|
||||
);
|
||||
const headerRight = (
|
||||
<TouchableItem
|
||||
accessibilityComponentType='button'
|
||||
accessibilityTraits='button'
|
||||
borderless={true}
|
||||
delayPressIn={0}
|
||||
pressColorAndroid='rgba(0, 0, 0, .32)'
|
||||
onPress={params.post ? params.post : emptyFunction}
|
||||
>
|
||||
<View style={styles.left}>
|
||||
<PaperPlane
|
||||
color={defalultTheme.sidebarHeaderTextColor}
|
||||
height={20}
|
||||
width={20}
|
||||
/>
|
||||
</View>
|
||||
</TouchableItem>
|
||||
);
|
||||
|
||||
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 (
|
||||
<ScrollView
|
||||
ref={this.getScrollViewRef}
|
||||
contentContainerStyle={styles.scrollView}
|
||||
style={styles.flex}
|
||||
>
|
||||
<TextInput
|
||||
ref={this.getInputRef}
|
||||
autoCapitalize='sentences'
|
||||
maxLength={MAX_MESSAGE_LENGTH}
|
||||
multiline={true}
|
||||
onBlur={this.handleBlur}
|
||||
onChangeText={this.handleTextChange}
|
||||
onFocus={this.handleFocus}
|
||||
placeholder={formatMessage({id: 'create_post.write', defaultMessage: 'Write a message...'})}
|
||||
placeholderTextColor={changeOpacity(defalultTheme.centerChannelColor, 0.5)}
|
||||
style={styles.input}
|
||||
underlineColorAndroid='transparent'
|
||||
value={value}
|
||||
/>
|
||||
{this.renderFiles()}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
renderChannelButton = () => {
|
||||
const {channelId} = this.state;
|
||||
|
||||
return (
|
||||
<ChannelButton
|
||||
channelId={channelId}
|
||||
onPress={this.goToChannels}
|
||||
theme={defalultTheme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderFiles = () => {
|
||||
const {files} = this.state;
|
||||
|
||||
return files.map((file, index) => {
|
||||
let component;
|
||||
|
||||
if (file.type.startsWith('image')) {
|
||||
component = (
|
||||
<View
|
||||
key={`item-${index}`}
|
||||
style={styles.imageContainer}
|
||||
>
|
||||
<Image
|
||||
source={{uri: file.fullPath, isStatic: true}}
|
||||
resizeMode='cover'
|
||||
style={styles.image}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else if (file.type.startsWith('video')) {
|
||||
component = (
|
||||
<View
|
||||
key={`item-${index}`}
|
||||
style={styles.imageContainer}
|
||||
>
|
||||
<Video
|
||||
ref={`video-${index}`}
|
||||
style={styles.video}
|
||||
resizeMode='cover'
|
||||
source={{uri: file.fullPath}}
|
||||
volume={0}
|
||||
paused={true}
|
||||
onLoad={() => this.refs[`video-${index}`].seek(0)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
let SvgIcon = extensionSvg[file.extension];
|
||||
if (!SvgIcon) {
|
||||
SvgIcon = GenericSvg;
|
||||
}
|
||||
|
||||
component = (
|
||||
<View
|
||||
key={`item-${index}`}
|
||||
style={styles.otherContainer}
|
||||
>
|
||||
<View style={styles.otherWrapper}>
|
||||
<View style={styles.fileIcon}>
|
||||
<SvgIcon
|
||||
width={19}
|
||||
height={48}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.fileContainer}
|
||||
key={`item-${index}`}
|
||||
>
|
||||
{component}
|
||||
<Text
|
||||
ellipsisMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.filename}
|
||||
>
|
||||
{file.filename}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
renderTeamButton = () => {
|
||||
const {teamId} = this.state;
|
||||
|
||||
return (
|
||||
<TeamButton
|
||||
onPress={this.goToTeams}
|
||||
teamId={teamId}
|
||||
theme={defalultTheme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {token, url} = this.props;
|
||||
const {hasPermission, files} = this.state;
|
||||
|
||||
if (hasPermission === false) {
|
||||
return (
|
||||
<View
|
||||
style={styles.flex}
|
||||
>
|
||||
<View style={styles.unauthenticatedContainer}>
|
||||
<Text style={styles.unauthenticated}>
|
||||
{formatMessage({
|
||||
id: 'mobile.extension.permission',
|
||||
defaultMessage: 'Mattermost needs access to the device storage to share files.'
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
} else if (files.length > 5) {
|
||||
return (
|
||||
<View
|
||||
style={styles.flex}
|
||||
>
|
||||
<View style={styles.unauthenticatedContainer}>
|
||||
<Text style={styles.unauthenticated}>
|
||||
{formatMessage({
|
||||
id: 'mobile.extension.file_limit',
|
||||
defaultMessage: 'Sharing is limited to a maximum of 5 files.'
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
} else if (token && url) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.wrapper}>
|
||||
{this.renderBody()}
|
||||
<View style={styles.flex}>
|
||||
{this.renderTeamButton()}
|
||||
{this.renderChannelButton()}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.unauthenticatedContainer}>
|
||||
<Text style={styles.unauthenticated}>
|
||||
{formatMessage({
|
||||
id: 'mobile.extension.authentication_required',
|
||||
defaultMessage: 'Authentication required: Please first login using the app.'
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
24
share_extension/android/extension_post/index.js
Normal file
24
share_extension/android/extension_post/index.js
Normal file
|
|
@ -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);
|
||||
16
share_extension/android/extension_post/team_button/index.js
Normal file
16
share_extension/android/extension_post/team_button/index.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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 (
|
||||
<TouchableHighlight
|
||||
onPress={onPress}
|
||||
style={styles.buttonContainer}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.2)}
|
||||
>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Text style={styles.buttonLabel}>
|
||||
{formatMessage({id: 'mobile.share_extension.team', defaultMessage: 'Team'})}
|
||||
</Text>
|
||||
<Text style={styles.buttonValue}>
|
||||
{teamName}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
});
|
||||
126
share_extension/android/extension_teams/extension_teams.js
Normal file
126
share_extension/android/extension_teams/extension_teams.js
Normal file
|
|
@ -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 (
|
||||
<View style={styles.separator}/>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = ({item}) => {
|
||||
const {navigation} = this.props;
|
||||
const {params} = navigation.state;
|
||||
|
||||
return (
|
||||
<TeamItem
|
||||
currentTeamId={params.currentTeamId}
|
||||
onSelectTeam={this.handleSelectTeam}
|
||||
teamId={item}
|
||||
theme={defaultTheme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const styles = getStyleSheet(defaultTheme);
|
||||
|
||||
if (this.state.loading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={this.props.teamIds}
|
||||
ItemSeparatorComponent={this.renderItemSeparator}
|
||||
renderItem={this.renderItem}
|
||||
keyExtractor={this.keyExtractor}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='on-drag'
|
||||
initialNumToRender={10}
|
||||
maxToRenderPerBatch={10}
|
||||
scrollEventThrottle={100}
|
||||
windowSize={5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
separator: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
28
share_extension/android/extension_teams/index.js
Normal file
28
share_extension/android/extension_teams/index.js
Normal file
|
|
@ -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);
|
||||
13
share_extension/android/extension_teams/team_item/index.js
Normal file
13
share_extension/android/extension_teams/team_item/index.js
Normal file
|
|
@ -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);
|
||||
123
share_extension/android/extension_teams/team_item/team_item.js
Normal file
123
share_extension/android/extension_teams/team_item/team_item.js
Normal file
|
|
@ -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 = (
|
||||
<View style={styles.checkmarkContainer}>
|
||||
<IonIcon
|
||||
name='md-checkmark'
|
||||
style={styles.checkmark}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const icon = (
|
||||
<View style={styles.iconContainer}>
|
||||
<Text style={styles.icon}>
|
||||
{team.display_name.substr(0, 2).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.item}>
|
||||
{icon}
|
||||
<Text
|
||||
style={[styles.text]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{team.display_name}
|
||||
</Text>
|
||||
{current}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
});
|
||||
62
share_extension/android/index.js
Normal file
62
share_extension/android/index.js
Normal file
|
|
@ -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 (
|
||||
<Provider store={this.store}>
|
||||
<IntlProvider
|
||||
locale={locale}
|
||||
messages={getTranslations(locale)}
|
||||
>
|
||||
<Navigation/>
|
||||
</IntlProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
41
share_extension/android/navigation.js
Normal file
41
share_extension/android/navigation.js
Normal file
|
|
@ -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;
|
||||
164
share_extension/android/selectors/index.js
Normal file
164
share_extension/android/selectors/index.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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: {
|
||||
14
share_extension/common/icons/index.js
Normal file
14
share_extension/common/icons/index.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<View style={styles.unauthenticatedContainer}>
|
||||
<Text style={styles.unauthenticated}>
|
||||
{'Authentication required: Please first login using the app.'}
|
||||
{formatMessage({
|
||||
id: 'mobile.extension.authentication_required',
|
||||
defaultMessage: 'Authentication required: Please first login using the app.'
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
53
yarn.lock
53
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"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue