Use @mattermost/react-native-paste-input to allow pasting of images & files (#5703) (#5704)

* Use @mattermost/react-native-paste-input to allow pasting of images & files

* upgrade @mattermost/react-native-paste-input

(cherry picked from commit 523777a207)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Mattermost Build 2021-09-30 14:50:16 +02:00 committed by GitHub
parent 38bddbf9fd
commit c94716353c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1697 additions and 2155 deletions

View file

@ -57,7 +57,6 @@ private final ReactNativeHost mReactNativeHost =
// Packages that cannot be auto linked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new RNNotificationsPackage(MainApplication.this));
packages.add(new RNPasteableTextInputPackage());
packages.add(
new TurboReactPackage() {
@Override

View file

@ -1,7 +0,0 @@
package com.mattermost.rnbeta;
import android.net.Uri;
public interface RNEditTextOnPasteListener {
void onPaste(Uri itemUri);
}

View file

@ -1,98 +0,0 @@
package com.mattermost.rnbeta;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
public class RNPasteableActionCallback implements ActionMode.Callback {
private final RNPasteableEditText mEditText;
RNPasteableActionCallback(RNPasteableEditText editText) {
mEditText = editText;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Bundle config = MainApplication.instance.getManagedConfig();
if (config != null) {
WritableMap result = Arguments.fromBundle(config);
String copyPasteProtection = result.getString("copyAndPasteProtection");
assert copyPasteProtection != null;
if (copyPasteProtection.equals("true")) {
disableMenus(menu);
}
}
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Uri uri = this.getUriInClipboard();
if (item.getItemId() == android.R.id.paste && uri != null) {
mEditText.getOnPasteListener().onPaste(uri);
mode.finish();
} else {
mEditText.onTextContextMenuItem(item.getItemId());
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
private void disableMenus(Menu menu) {
for (int i = 0; i < menu.size(); i++) {
MenuItem item = menu.getItem(i);
int id = item.getItemId();
boolean shouldDisableMenu = (
id == android.R.id.paste
|| id == android.R.id.copy
|| id == android.R.id.cut
);
item.setEnabled(!shouldDisableMenu);
}
}
private Uri getUriInClipboard() {
ClipboardManager clipboardManager = (ClipboardManager) mEditText.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData == null) {
return null;
}
ClipData.Item item = clipData.getItemAt(0);
if (item == null) {
return null;
}
CharSequence chars = item.getText();
if (chars == null) {
return null;
}
String text = chars.toString();
if (text.length() > 0) {
return null;
}
return item.getUri();
}
}

View file

@ -1,22 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import com.facebook.react.views.textinput.ReactEditText;
public class RNPasteableEditText extends ReactEditText {
private RNEditTextOnPasteListener mOnPasteListener;
public RNPasteableEditText(Context context) {
super(context);
}
public void setOnPasteListener(RNEditTextOnPasteListener listener) {
mOnPasteListener = listener;
}
public RNEditTextOnPasteListener getOnPasteListener() {
return mOnPasteListener;
}
}

View file

@ -1,163 +0,0 @@
package com.mattermost.rnbeta;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.Build;
import android.util.Patterns;
import android.webkit.MimeTypeMap;
import android.webkit.URLUtil;
import androidx.annotation.RequiresApi;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.mattermost.share.RealPathUtil;
import com.mattermost.share.ShareModule;
import java.io.FileNotFoundException;
import java.io.File;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.regex.Matcher;
public class RNPasteableEditTextOnPasteListener implements RNEditTextOnPasteListener {
private final RNPasteableEditText mEditText;
RNPasteableEditTextOnPasteListener(RNPasteableEditText editText) {
mEditText = editText;
}
@Override
public void onPaste(Uri itemUri) {
ReactContext reactContext = (ReactContext)mEditText.getContext();
String uri = itemUri.toString();
WritableArray images = null;
WritableMap error = null;
String uriMimeType = reactContext.getContentResolver().getType(itemUri);
if (uriMimeType == null) {
return;
}
// Special handle for Google docs
if (uri.equals("content://com.google.android.apps.docs.editors.kix.editors.clipboard")) {
ClipboardManager clipboardManager = (ClipboardManager) reactContext.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData == null) {
return;
}
ClipData.Item item = clipData.getItemAt(0);
String htmlText = item.getHtmlText();
// Find uri from html
Matcher matcher = Patterns.WEB_URL.matcher(htmlText);
if (matcher.find()) {
uri = htmlText.substring(matcher.start(1), matcher.end());
}
}
if (uri.startsWith("http")) {
Thread pastImageFromUrlThread = new Thread(new RNPasteableImageFromUrl(reactContext, mEditText, uri));
pastImageFromUrlThread.start();
return;
}
uri = RealPathUtil.getRealPathFromURI(reactContext, itemUri);
if (uri == null) {
return;
}
// Get type
String extension = MimeTypeMap.getFileExtensionFromUrl(uri);
if (extension == null) {
return;
}
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mimeType == null) {
return;
}
// Get fileName
String fileName = URLUtil.guessFileName(uri, null, mimeType);
if (uri.contains(ShareModule.CACHE_DIR_NAME) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
uri = moveToImagesCache(uri, fileName);
}
if (uri == null) {
return;
}
// Get fileSize
long fileSize;
try {
ContentResolver contentResolver = reactContext.getContentResolver();
AssetFileDescriptor assetFileDescriptor = contentResolver.openAssetFileDescriptor(itemUri, "r");
if (assetFileDescriptor == null) {
return;
}
fileSize = assetFileDescriptor.getLength();
WritableMap image = Arguments.createMap();
image.putString("type", mimeType);
image.putDouble("fileSize", fileSize);
image.putString("fileName", fileName);
image.putString("uri", "file://" + uri);
images = Arguments.createArray();
images.pushMap(image);
} catch (FileNotFoundException e) {
error = Arguments.createMap();
error.putString("message", e.getMessage());
}
WritableMap event = Arguments.createMap();
event.putArray("data", images);
event.putMap("error", error);
reactContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(
mEditText.getId(),
"onPaste",
event
);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private String moveToImagesCache(String src, String fileName) {
ReactContext ctx = (ReactContext)mEditText.getContext();
String cacheFolder = ctx.getCacheDir().getAbsolutePath() + "/Images/";
String dest = cacheFolder + fileName;
File folder = new File(cacheFolder);
try {
if (!folder.exists()) {
boolean created = folder.mkdirs();
if (!created) {
return null;
}
}
Files.move(Paths.get(src), Paths.get(dest));
} catch (FileAlreadyExistsException fileError) {
// Do nothing and return dest path
} catch (Exception err) {
return null;
}
return dest;
}
}

View file

@ -1,76 +0,0 @@
package com.mattermost.rnbeta;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.views.textinput.ReactEditText;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
public class RNPasteableImageFromUrl implements Runnable {
private final ReactContext mContext;
private final String mUri;
private final ReactEditText mTarget;
RNPasteableImageFromUrl(ReactContext context, ReactEditText target, String uri) {
mContext = context;
mUri = uri;
mTarget = target;
}
@Override
public void run() {
WritableArray images = null;
WritableMap error = null;
try {
URL url = new URL(mUri);
URLConnection u = url.openConnection();
// Get type
String mimeType = u.getHeaderField("Content-Type");
if (!mimeType.startsWith("image")) {
return;
}
// Get fileSize
long fileSize = Long.parseLong(u.getHeaderField("Content-Length"));
// Get fileName
String contentDisposition = u.getHeaderField("Content-Disposition");
int startIndex = contentDisposition.indexOf("filename=\"") + 10;
int endIndex = contentDisposition.length() - 1;
String fileName = contentDisposition.substring(startIndex, endIndex);
WritableMap image = Arguments.createMap();
image.putString("type", mimeType);
image.putDouble("fileSize", fileSize);
image.putString("fileName", fileName);
image.putString("uri", mUri);
images = Arguments.createArray();
images.pushMap(image);
} catch (IOException e) {
error = Arguments.createMap();
error.putString("message", e.getMessage());
}
WritableMap event = Arguments.createMap();
event.putArray("data", images);
event.putMap("error", error);
mContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(
mTarget.getId(),
"onPaste",
event
);
}
}

View file

@ -1,85 +0,0 @@
package com.mattermost.rnbeta;
import androidx.annotation.NonNull;
import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
import android.text.InputType;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.views.textinput.ReactEditText;
import com.facebook.react.views.textinput.ReactTextInputManager;
import java.util.Map;
import javax.annotation.Nullable;
public class RNPasteableTextInputManager extends ReactTextInputManager {
@Override
@NonNull
public String getName() {
return "PasteableTextInputAndroid";
}
@Override
@NonNull
public ReactEditText createViewInstance(ThemedReactContext context) {
RNPasteableEditText editText = new RNPasteableEditText(context) {
@Override
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
final InputConnection ic = super.onCreateInputConnection(editorInfo);
EditorInfoCompat.setContentMimeTypes(editorInfo,
new String [] {"image/*"});
final InputConnectionCompat.OnCommitContentListener callback =
(inputContentInfo, flags, opts) -> {
// read and display inputContentInfo asynchronously
if ((flags &
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
}
catch (Exception e) {
return false;
}
}
this.getOnPasteListener().onPaste(inputContentInfo.getContentUri());
return true;
};
return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
}
};
int inputType = editText.getInputType();
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
editText.setReturnKeyType("done");
editText.setCustomInsertionActionModeCallback(new RNPasteableActionCallback(editText));
editText.setCustomSelectionActionModeCallback(new RNPasteableActionCallback(editText));
return editText;
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, ReactEditText editText) {
super.addEventEmitters(reactContext, editText);
RNPasteableEditText pasteableEditText = (RNPasteableEditText)editText;
pasteableEditText.setOnPasteListener(new RNPasteableEditTextOnPasteListener(pasteableEditText));
}
@Nullable
@Override
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
Map<String, Object> map = super.getExportedCustomBubblingEventTypeConstants();
assert map != null;
map.put(
"onPaste",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onPaste")));
return map;
}
}

View file

@ -1,28 +0,0 @@
package com.mattermost.rnbeta;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
public class RNPasteableTextInputPackage implements ReactPackage {
@Nonnull
@Override
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Nonnull
@Override
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
return Arrays.asList(
new RNPasteableTextInputManager()
);
}
}

View file

@ -1,10 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PasteableTextInput should render pasteable text input 1`] = `
<TextInput
onPaste={[Function]}
screenId="Channel"
>
My Text
</TextInput>
`;

View file

@ -1,77 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {NativeEventEmitter, NativeModules, Platform, TextInput} from 'react-native';
import {PASTE_FILES} from '@constants/post_draft';
import EventEmitter from '@mm-redux/utils/event_emitter';
const {OnPasteEventManager} = NativeModules;
const OnPasteEventEmitter = new NativeEventEmitter(OnPasteEventManager);
export class PasteableTextInput extends React.PureComponent {
static propTypes = {
...TextInput.PropTypes,
forwardRef: PropTypes.any,
screenId: PropTypes.string.isRequired,
}
componentDidMount() {
this.subscription = OnPasteEventEmitter.addListener('onPaste', this.onPaste);
}
componentWillUnmount() {
if (this.subscription) {
this.subscription.remove();
}
}
getLastSubscriptionKey = () => {
const subscriptions = OnPasteEventEmitter._subscriber._subscriptionsForType.onPaste?.filter((sub) => sub); // eslint-disable-line no-underscore-dangle
return subscriptions?.length && subscriptions[subscriptions.length - 1].key;
}
onPaste = (event) => {
const lastSubscriptionKey = this.getLastSubscriptionKey();
if (this.subscription.key !== lastSubscriptionKey) {
return;
}
let data = null;
let error = null;
if (Platform.OS === 'android') {
const {nativeEvent} = event;
data = nativeEvent.data;
error = nativeEvent.error;
} else {
data = event;
}
EventEmitter.emit(PASTE_FILES, error, data, this.props.screenId);
}
render() {
const {testID, forwardRef, ...props} = this.props;
return (
<TextInput
testID={testID}
{...props}
onPaste={this.onPaste}
ref={forwardRef}
/>
);
}
}
const WrappedPasteableTextInput = (props, ref) => (
<PasteableTextInput
{...props}
forwardRef={ref}
/>
);
export default React.forwardRef(WrappedPasteableTextInput);

View file

@ -1,33 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {PasteableTextInput} from './index';
describe('PasteableTextInput', () => {
test('should render pasteable text input', () => {
const onPaste = jest.fn();
const text = 'My Text';
const component = shallow(
<PasteableTextInput
onPaste={onPaste}
screenId='Channel'
>{text}</PasteableTextInput>,
);
expect(component).toMatchSnapshot();
});
test('should remove onPaste listener when unmount', () => {
const mockRemove = jest.fn();
const text = 'My Text';
const component = shallow(
<PasteableTextInput screenId='Channel'>{text}</PasteableTextInput>,
);
component.instance().subscription.remove = mockRemove;
component.instance().componentWillUnmount();
expect(mockRemove).toHaveBeenCalled();
});
});

View file

@ -284,31 +284,49 @@ exports[`PostDraft Should render the DraftInput 1`] = `
}
>
<View>
<TextInput
<PasteInput
accessible={true}
autoCapitalize="sentences"
autoCompleteType="off"
blurOnSubmit={false}
disableCopyPaste={false}
disableFullscreenUI={true}
focusable={true}
keyboardAppearance="light"
keyboardType="default"
mostRecentEventCount={0}
multiline={true}
onBlur={[Function]}
onChange={[Function]}
onChangeText={[Function]}
onClick={[Function]}
onEndEditing={[Function]}
onFocus={[Function]}
onPaste={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onSelectionChange={[Function]}
onStartShouldSetResponder={[Function]}
placeholder="Write to "
placeholderTextColor="rgba(63,67,80,0.5)"
screenId="NavigationScreen1"
style={
Object {
"color": "#3f4350",
"fontSize": 15,
"lineHeight": 20,
"maxHeight": 150,
"minHeight": 30,
"paddingBottom": 6,
"paddingHorizontal": 12,
"paddingTop": 6,
}
Array [
Object {
"color": "#3f4350",
"fontSize": 15,
"lineHeight": 20,
"maxHeight": 150,
"minHeight": 30,
"paddingBottom": 6,
"paddingHorizontal": 12,
"paddingTop": 6,
},
]
}
testID="post_draft.post.input"
textContentType="none"

View file

@ -1,15 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PostInput should match, full snapshot 1`] = `
<ForwardRef(WrappedPasteableTextInput)
<ForwardRef
autoCompleteType="off"
blurOnSubmit={false}
disableCopyPaste={false}
disableFullscreenUI={true}
keyboardAppearance="light"
keyboardType="default"
multiline={true}
onChangeText={[Function]}
onEndEditing={[Function]}
onPaste={[Function]}
onSelectionChange={[Function]}
placeholder="Write to Test Channel"
placeholderTextColor="rgba(63,67,80,0.5)"

View file

@ -6,10 +6,11 @@ import React, {PureComponent} from 'react';
import {intlShape} from 'react-intl';
import {Alert, AppState, findNodeHandle, Keyboard, NativeModules, Platform} from 'react-native';
import PasteableTextInput from '@components/pasteable_text_input';
import {NavigationTypes} from '@constants';
import DEVICE from '@constants/device';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT, PASTE_FILES} from '@constants/post_draft';
import mattermostManaged from '@mattermost-managed';
import PasteableTextInput from '@mattermost/react-native-paste-input';
import {debounce} from '@mm-redux/actions/helpers';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {t} from '@utils/i18n';
@ -55,6 +56,7 @@ export default class PostInput extends PureComponent {
this.state = {
keyboardType: 'default',
longMessageAlertShown: false,
disableCopyAndPaste: mattermostManaged.getCachedConfig()?.copyAndPasteProtection === 'true',
};
}
@ -63,6 +65,7 @@ export default class PostInput extends PureComponent {
EventEmitter.on(event, this.handleInsertTextToDraft);
EventEmitter.on(NavigationTypes.BLUR_POST_DRAFT, this.blur);
this.appStateListener = AppState.addEventListener('change', this.handleAppStateChange);
this.managedListener = mattermostManaged.addEventListener('managedConfigDidChange', this.onManagedConfigurationChange);
if (Platform.OS === 'android') {
this.keyboardListener = Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard);
@ -73,6 +76,7 @@ export default class PostInput extends PureComponent {
const event = this.props.rootId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT;
EventEmitter.off(NavigationTypes.BLUR_POST_DRAFT, this.blur);
EventEmitter.off(event, this.handleInsertTextToDraft);
mattermostManaged.removeEventListener(this.managedListener);
this.appStateListener.remove();
if (Platform.OS === 'android') {
@ -249,11 +253,20 @@ export default class PostInput extends PureComponent {
}
this.value = completed;
this.input.current.setNativeProps({
text: completed,
});
};
onManagedConfigurationChange = (config) => {
this.setState({disableCopyAndPaste: config.copyAndPasteProtection === 'true'});
}
onPaste = (error, files) => {
EventEmitter.emit(PASTE_FILES, error, files, this.props.screenId);
}
resetTextInput = () => {
if (this.input.current) {
this.input.current.setNativeProps({
@ -287,6 +300,7 @@ export default class PostInput extends PureComponent {
<PasteableTextInput
testID={testID}
ref={this.input}
disableCopyPaste={this.state.disableCopyAndPaste}
style={{...style.input, maxHeight}}
onChangeText={this.handleTextChange}
onSelectionChange={this.handlePostDraftSelectionChanged}
@ -297,6 +311,7 @@ export default class PostInput extends PureComponent {
underlineColorAndroid='transparent'
keyboardType={this.state.keyboardType}
onEndEditing={this.handleEndEditing}
onPaste={this.onPaste}
disableFullscreenUI={true}
textContentType='none'
autoCompleteType='off'

View file

@ -17,9 +17,7 @@
3D7B4E6EE6B948AAA6A1E4E6 /* Roboto-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C46342B0E5FD4D878AF3A129 /* Roboto-BoldItalic.ttf */; };
460A48EA5C6C4D8B8D9A2C75 /* Roboto-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 031EF04FB2D14EEFAACBAA1A /* Roboto-Italic.ttf */; };
4953BF602368AE8600593328 /* SwimeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4953BF5F2368AE8600593328 /* SwimeProxy.swift */; };
499F7A5F2354F49F00E7AF6E /* UIPasteboard+GetImageInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 499F7A5E2354F49F00E7AF6E /* UIPasteboard+GetImageInfo.m */; };
499F7A622354F4CC00E7AF6E /* NSData+MimeType.m in Sources */ = {isa = PBXBuildFile; fileRef = 499F7A602354F4CC00E7AF6E /* NSData+MimeType.m */; };
499F7A652354F51900E7AF6E /* OnPasteEventManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 499F7A642354F51900E7AF6E /* OnPasteEventManager.m */; };
49B4C050230C981C006E919E /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; };
531BEBC62513E93C00BC05B1 /* compass-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 531BEBC52513E93C00BC05B1 /* compass-icons.ttf */; };
531BEBC72513E93C00BC05B1 /* compass-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 531BEBC52513E93C00BC05B1 /* compass-icons.ttf */; };
@ -44,7 +42,6 @@
7F581D35221ED5C60099E66B /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F581D34221ED5C60099E66B /* NotificationService.swift */; };
7F581D39221ED5C60099E66B /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7F581D32221ED5C60099E66B /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
7F581F78221EEA7C0099E66B /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; };
7F5BA34722B99B7B005B05D3 /* Mattermost+RCTUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F5BA34622B99B7B005B05D3 /* Mattermost+RCTUITextView.m */; };
7F72F2EE2211220500F98FFF /* GenericPreview.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F72F2ED2211220500F98FFF /* GenericPreview.xib */; };
7F72F2F0221123E200F98FFF /* GenericPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F72F2EF221123E200F98FFF /* GenericPreview.swift */; };
7F72F2F322112EC700F98FFF /* generic.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F72F2F222112EC700F98FFF /* generic.png */; };
@ -162,12 +159,8 @@
4953BF5F2368AE8600593328 /* SwimeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SwimeProxy.swift; path = Mattermost/SwimeProxy.swift; sourceTree = "<group>"; };
495BC95F23565ABF00C40C83 /* libXCDYouTubeKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libXCDYouTubeKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
495BC96123565ADD00C40C83 /* libYoutubePlayer-in-WKWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libYoutubePlayer-in-WKWebView.a"; sourceTree = BUILT_PRODUCTS_DIR; };
499F7A5D2354F49F00E7AF6E /* UIPasteboard+GetImageInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIPasteboard+GetImageInfo.h"; path = "Mattermost/UIPasteboard+GetImageInfo.h"; sourceTree = "<group>"; };
499F7A5E2354F49F00E7AF6E /* UIPasteboard+GetImageInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIPasteboard+GetImageInfo.m"; path = "Mattermost/UIPasteboard+GetImageInfo.m"; sourceTree = "<group>"; };
499F7A602354F4CC00E7AF6E /* NSData+MimeType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSData+MimeType.m"; path = "Mattermost/NSData+MimeType.m"; sourceTree = "<group>"; };
499F7A612354F4CC00E7AF6E /* NSData+MimeType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSData+MimeType.h"; path = "Mattermost/NSData+MimeType.h"; sourceTree = "<group>"; };
499F7A632354F51900E7AF6E /* OnPasteEventManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OnPasteEventManager.h; path = Mattermost/OnPasteEventManager.h; sourceTree = "<group>"; };
499F7A642354F51900E7AF6E /* OnPasteEventManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OnPasteEventManager.m; path = Mattermost/OnPasteEventManager.m; sourceTree = "<group>"; };
499F7AF0235511FC00E7AF6E /* Mattermost-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Mattermost-Regular.otf"; path = "../assets/fonts/Mattermost-Regular.otf"; sourceTree = "<group>"; };
499F7B3F235513F600E7AF6E /* libXCDYouTubeKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libXCDYouTubeKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
499F7B412355141200E7AF6E /* libYoutubePlayer-in-WKWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libYoutubePlayer-in-WKWebView.a"; sourceTree = BUILT_PRODUCTS_DIR; };
@ -204,8 +197,6 @@
7F581D34221ED5C60099E66B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
7F581D36221ED5C60099E66B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
7F581F77221EEA5A0099E66B /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = "<group>"; };
7F5BA34522B99B7B005B05D3 /* Mattermost+RCTUITextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Mattermost+RCTUITextView.h"; path = "Mattermost/Mattermost+RCTUITextView.h"; sourceTree = "<group>"; };
7F5BA34622B99B7B005B05D3 /* Mattermost+RCTUITextView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "Mattermost+RCTUITextView.m"; path = "Mattermost/Mattermost+RCTUITextView.m"; sourceTree = "<group>"; };
7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = "<group>"; };
7F72F2ED2211220500F98FFF /* GenericPreview.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = GenericPreview.xib; sourceTree = "<group>"; };
7F72F2EF221123E200F98FFF /* GenericPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPreview.swift; sourceTree = "<group>"; };
@ -330,12 +321,8 @@
children = (
536CC6C223E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.h */,
536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */,
499F7A632354F51900E7AF6E /* OnPasteEventManager.h */,
499F7A642354F51900E7AF6E /* OnPasteEventManager.m */,
499F7A612354F4CC00E7AF6E /* NSData+MimeType.h */,
499F7A602354F4CC00E7AF6E /* NSData+MimeType.m */,
499F7A5D2354F49F00E7AF6E /* UIPasteboard+GetImageInfo.h */,
499F7A5E2354F49F00E7AF6E /* UIPasteboard+GetImageInfo.m */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
13B07FB01A68108700A75B9A /* AppDelegate.m */,
7FEB10961F6101710039A015 /* BlurAppScreen.h */,
@ -354,8 +341,6 @@
7FEB109B1F61019C0039A015 /* UIImage+ImageEffects.h */,
7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */,
7F151D40221B069200FAD8F3 /* 0155-keys.png */,
7F5BA34522B99B7B005B05D3 /* Mattermost+RCTUITextView.h */,
7F5BA34622B99B7B005B05D3 /* Mattermost+RCTUITextView.m */,
7F0F4B0924BA173900E14C60 /* LaunchScreen.storyboard */,
);
name = Mattermost;
@ -824,11 +809,8 @@
7FEB109E1F61019C0039A015 /* UIImage+ImageEffects.m in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
7FEB10981F6101710039A015 /* BlurAppScreen.m in Sources */,
499F7A5F2354F49F00E7AF6E /* UIPasteboard+GetImageInfo.m in Sources */,
7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */,
499F7A652354F51900E7AF6E /* OnPasteEventManager.m in Sources */,
7F240ACD220D460300637665 /* MattermostBucketModule.m in Sources */,
7F5BA34722B99B7B005B05D3 /* Mattermost+RCTUITextView.m in Sources */,
536CC6C323E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View file

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

View file

@ -1,62 +0,0 @@
//
// Mattermost+RCTUITextView.m
// Mattermost
//
// Created by Elias Nahum on 6/18/19.
// Copyright © 2019 Facebook. All rights reserved.
//
#import "Mattermost+RCTUITextView.h"
#import <React/RCTUITextView.h>
#import <React/RCTUtils.h>
#import <React/RCTMultilineTextInputView.h>
#import "OnPasteEventManager.h"
#import "UIPasteboard+GetImageInfo.h"
@implementation Mattermost_RCTUITextView
@end
@implementation RCTUITextView (DisableCopyPaste)
#pragma GCC diagnostic ignored "-Wundeclared-selector"
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
NSDictionary *response = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"com.apple.configuration.managed"];
if(response) {
NSString *copyPasteProtection = response[@"copyAndPasteProtection"];
BOOL prevent = action == @selector(paste:) ||
action == @selector(copy:) ||
action == @selector(cut:) ||
action == @selector(_share:);
if ([copyPasteProtection isEqual: @"true"] && prevent) {
return NO;
}
}
if (action == @selector(paste:) && [UIPasteboard generalPasteboard].numberOfItems > 0) {
return true;
}
return [super canPerformAction:action withSender:sender];
}
-(void)paste:(id)sender {
[super paste:sender];
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
if (pasteboard.hasURLs || pasteboard.hasStrings || pasteboard.hasColors) {
return;
}
NSArray<NSDictionary *> *files = [pasteboard getCopiedFiles];
if (files != nil && files.count > 0) {
[OnPasteEventManager pasteFiles:files];
}
// Dismiss contextual menu
[self resignFirstResponder];
}
@end

View file

@ -186,4 +186,12 @@ RCT_EXPORT_METHOD(supportsFaceId:(RCTPromiseResolveBlock)resolve
});
}
RCT_EXPORT_METHOD(addListener:(NSString *)eventName) {
// Keep: Required for RN built in Event Emitter Calls.
}
RCT_EXPORT_METHOD(removeListeners:(double)count) {
// Keep: Required for RN built in Event Emitter Calls.
}
@end

View file

@ -1,20 +0,0 @@
//
// PasteEventManager.h
// Mattermost
//
// Created by Tek Min Ewe on 05/08/2019.
// Copyright © 2019 Facebook. All rights reserved.
//
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
NS_ASSUME_NONNULL_BEGIN
@interface OnPasteEventManager : RCTEventEmitter<RCTBridgeModule>
+(void)pasteFiles:(NSArray<NSDictionary *> *)data;
@end
NS_ASSUME_NONNULL_END

View file

@ -1,47 +0,0 @@
//
// PasteEventManager.m
// Mattermost
//
// Created by Tek Min Ewe on 05/08/2019.
// Copyright © 2019 Facebook. All rights reserved.
//
#import "OnPasteEventManager.h"
@implementation OnPasteEventManager {
bool hasListeners;
}
RCT_EXPORT_MODULE();
-(void)startObserving {
hasListeners = YES;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPaste:) name:@"onPaste" object:nil];
}
-(void)stopObserving {
hasListeners = NO;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(NSArray<NSString *>*)supportedEvents {
return @[@"onPaste"];
}
-(void)onPaste:(NSNotification *)data {
if (!hasListeners) {
return;
}
[self sendEventWithName:@"onPaste" body:data.userInfo[@"data"]];
}
+(void)pasteFiles:(NSArray<NSDictionary *> *)data {
[[NSNotificationCenter defaultCenter] postNotificationName:@"onPaste" object:data userInfo:@{
@"data": data
}];
}
@end

View file

@ -1,19 +0,0 @@
//
// UIPasteboard+GetImageInfo.h
// Mattermost
//
// Created by Tek Min Ewe on 05/08/2019.
// Copyright © 2019 Facebook. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIPasteboard (GetImageInfo)
-(NSArray<NSDictionary *> *)getCopiedFiles;
@end
NS_ASSUME_NONNULL_END

View file

@ -1,104 +0,0 @@
//
// UIPasteboard+GetImageInfo.m
// Mattermost
//
// Created by Tek Min Ewe on 05/08/2019.
// Copyright © 2019 Facebook. All rights reserved.
//
#import "UIPasteboard+GetImageInfo.h"
#import "NSData+MimeType.h"
#import "Mattermost-Swift.h"
#import "UIImage+vImageScaling.h"
@implementation UIPasteboard (GetImageInfo)
-(NSArray<NSDictionary *> *)getCopiedFiles {
NSMutableArray<NSDictionary *> *fileInfos = [[NSMutableArray alloc] init];
NSArray<NSDictionary<NSString *,id> *> *items = self.items;
for (int i = 0; i < items.count; i++) {
NSDictionary *item = items[i];
BOOL added = NO;
for (int j = 0; j < item.allKeys.count; j++) {
if (added) {
continue;
}
NSString *type = item.allKeys[j];
@try {
NSString *uri = self.string;
NSData *fileData = item[type];
if ([type isEqual:@"public.jpeg"] || [type isEqual:@"public.heic"] || [type isEqual:@"public.png"]) {
fileData = [self getDataForImageItem:item[type] type:type];
} else if ([type isEqual:@"com.compuserve.gif"]) {
fileData = [self dataForPasteboardType:type];
}
SwimeProxy *swimeProxy = [SwimeProxy shared];
MimeTypeProxy *mimeProxy = [swimeProxy getMimeAndExtensionWithData:fileData uti:type];
NSString *extension;
NSString *mimeType;
if (mimeProxy != nil) {
extension = mimeProxy.ext;
mimeType = mimeProxy.mime;
} else {
extension = [fileData extension];
mimeType = [fileData mimeType];
}
if ([extension length] == 0) {
continue;
}
NSString *tempFilename = [NSString stringWithFormat:@"%@.%@", [[NSProcessInfo processInfo] globallyUniqueString], extension];
NSURL *tempFileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:tempFilename]];
BOOL success = [fileData writeToURL:tempFileURL atomically:YES];
if (success) {
added = YES;
uri = tempFileURL.absoluteString;
[fileInfos addObject:@{
@"fileName": tempFilename,
@"fileSize": @([fileData length]),
@"type": mimeType,
@"uri": uri,
}];
}
} @catch (NSException *exception) {
[fileInfos addObject:@{
@"type": type,
@"error": exception.reason,
}];
}
}
}
return fileInfos;
}
-(NSData *) getDataForImageItem:(NSData *)imageData type:(NSString *)type {
UIImage *image;
if ([type isEqual:@"public.heic"]) {
CFDataRef cfdata = CFDataCreate(NULL, [imageData bytes], [imageData length]);
CGImageSourceRef source = CGImageSourceCreateWithData(cfdata, nil);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, nil);
image = [[UIImage alloc] initWithCGImage:imageRef];
} else {
image = (UIImage *)imageData;
}
size_t width = CGImageGetWidth(image.CGImage);
size_t height = CGImageGetHeight(image.CGImage);
if (width > 6048 || height > 4032) {
image = [image vImageScaledImageWithSize:CGSizeMake(2048, 2048) contentMode:UIViewContentModeScaleAspectFit];
}
if ([type isEqual:@"public.png"]) {
return UIImagePNGRepresentation(image);
}
return UIImageJPEGRepresentation(image, 1.0);
}
@end

View file

@ -258,6 +258,9 @@ PODS:
- React-Core
- react-native-passcode-status (1.1.2):
- React
- react-native-paste-input (0.2.0):
- React-Core
- Swime (= 3.0.6)
- react-native-safe-area (0.5.1):
- React
- react-native-safe-area-context (3.3.2):
@ -476,6 +479,7 @@ DEPENDENCIES:
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-notifications (from `../node_modules/react-native-notifications`)
- react-native-passcode-status (from `../node_modules/react-native-passcode-status`)
- "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
- react-native-safe-area (from `../node_modules/react-native-safe-area`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-startup-time (from `../node_modules/react-native-startup-time`)
@ -604,6 +608,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-notifications"
react-native-passcode-status:
:path: "../node_modules/react-native-passcode-status"
react-native-paste-input:
:path: "../node_modules/@mattermost/react-native-paste-input"
react-native-safe-area:
:path: "../node_modules/react-native-safe-area"
react-native-safe-area-context:
@ -737,6 +743,7 @@ SPEC CHECKSUMS:
react-native-netinfo: 92e6e4476eb8bf6fc2d7c0a6ca0a1406f663d73a
react-native-notifications: 97c14bf84c64bd6a6eb7bdcdb916036d93d33428
react-native-passcode-status: e78f76b3c8db613e6ced6bd40b54aa4f53374173
react-native-paste-input: 5da631cf8210a9e61d577f987f7fb8b0ef966121
react-native-safe-area: e8230b0017d76c00de6b01e2412dcf86b127c6a3
react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057
react-native-startup-time: 1a068b744ce5097a85ebe0fbff691b05961c324d

2873
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@
"private": true,
"dependencies": {
"@babel/runtime": "7.15.4",
"@mattermost/react-native-paste-input": "0.2.0",
"@react-native-community/async-storage": "1.12.1",
"@react-native-community/cameraroll": "4.0.4",
"@react-native-community/clipboard": "1.5.1",

View file

@ -19,19 +19,6 @@ index 8eca425..f0df04f 100644
<NativeDirectionalScrollView
{...props}
style={StyleSheet.compose(baseStyle, inner)}
diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js
index b8d559a..cb84238 100644
--- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js
+++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js
@@ -547,7 +547,7 @@ export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
});
let AndroidTextInputNativeComponent = NativeComponentRegistry.get<NativeProps>(
- 'AndroidTextInput',
+ 'PasteableTextInputAndroid',
() => AndroidTextInputViewConfig,
);
diff --git a/node_modules/react-native/Libraries/Lists/VirtualizedList.js b/node_modules/react-native/Libraries/Lists/VirtualizedList.js
index a7c1567..1531a45 100644
--- a/node_modules/react-native/Libraries/Lists/VirtualizedList.js