Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions .eslintrc.js

This file was deleted.

7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ DerivedData
*.ipa
*.xcuserstate
project.xcworkspace
.xcode.env.local
example/ios/.xcode.env.local

# Android/IJ
#
Expand All @@ -36,6 +38,7 @@ project.xcworkspace
local.properties
android.iml
.cxx
.kotlin

# Cocoapods
#
Expand All @@ -60,5 +63,9 @@ android/keystores/debug.keystore
# generated by bob
lib/

# Codegen generated files (Fabric)
ios/PasteTextInputSpecs/
android/build/generated/

# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
194 changes: 180 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,112 @@
# @mattermost/react-native-paste-input

React Native `TextInput` component have functionality to capture text input from a user
React Native `TextInput` component has functionality to capture text input from a user
by using the soft and hardware keyboards but lacks the ability to restrict copy & paste options
as well as allwing pasting different files formats copied from other apps, like images & videos from
as well as allowing pasting different file formats copied from other apps, like images & videos from
the Photos gallery app.

`PasteInput` is a `TextInput` replacement that solves this issues.
`PasteInput` is a `TextInput` replacement that solves these issues.

## Requirements

- **React Native >= 0.76.0** (Fabric/New Architecture only)
- **iOS >= 13.4**
- **Android minSdkVersion >= 21**

## Installation

```sh
npm i --save-exact @mattermost/react-native-paste-input
npm install --save-exact @mattermost/react-native-paste-input
```

### iOS Setup (Required)

You need to call `PasteInputModule.setup` from your `AppDelegate` so the library can locate native views. Choose the snippet that matches your AppDelegate style.

#### AppDelegate.swift

```swift
import UIKit
import React
import React_RCTAppDelegate
import ReactAppDependencyProvider
import react_native_paste_input

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var reactNativeDelegate: ReactNativeDelegate?
var reactNativeFactory: RCTReactNativeFactory?

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let delegate = ReactNativeDelegate()
let factory = RCTReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()

reactNativeDelegate = delegate
reactNativeFactory = factory

window = UIWindow(frame: UIScreen.main.bounds)

factory.startReactNative(
withModuleName: "YourAppName",
in: window,
launchOptions: launchOptions
)

PasteInputModule.setup(factory.rootViewFactory)

return true
}
}

class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
override func bundleURL() -> URL? {
#if DEBUG
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}
```

#### AppDelegate.mm

```objc
// AppDelegate.mm
#import <React/RCTAppDelegate.h>
#import <react_native_paste_input/PasteInputModule.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.moduleName = @"YourAppName";
BOOL result = [super application:application didFinishLaunchingWithOptions:launchOptions];

[PasteInputModule setup:self.rootViewFactory];

return result;
}

@end
```

**Note:** Without this call the library cannot locate native views by tag and paste events will not be delivered.

### Android Setup

No additional setup required - autolinking will handle everything.

## Demo
| Android | iOS |
|-- |-- |
|---------|-----|
|![Android](/example/gifs/AndroidPasteInput.gif)|![iOS](/example/gifs/iOSPasteInput.gif)|



## Usage

```js
Expand All @@ -33,8 +120,13 @@ const YourTextInput = () => {
error: string | null | undefined,
files: Array<PastedFile>
) => {
console.log('ERROR', error);
if (error) {
console.error('Paste error:', error);
return;
}

console.log('PASTED FILES', files);
// files is an array of { fileName, fileSize, type, uri }
};

return (
Expand All @@ -48,21 +140,95 @@ const YourTextInput = () => {
keyboardType="default"
disableFullscreenUI={true}
textContentType="none"
autoCompleteType="off"
autoComplete="off"
/>
);
}
```

## API

### Properties
All properties of the [TextInput](!https://reactnative.dev/docs/textinput) component plus:

##### `disableCopyPaste: boolean`
All properties of the [TextInput](https://reactnative.dev/docs/textinput) component plus:

#### `disableCopyPaste: boolean`
Indicates if the menu items for *cut*, *copy*, *paste* and *share* should not be present in the context menu.

##### `onPaste: (error, files) => void`
Callback that is called when the pasting files into the text input.
*Note: On Android this callback is also called when selecting and image / gif from the soft keyboard.*
**Default:** `false`

#### `onPaste: (error: string | null, files: PastedFile[]) => void`
Callback that is called when pasting files into the text input.

**Note:** On Android, this callback is also called when selecting an image/GIF from the soft keyboard.

**Parameters:**
- `error`: Error message if paste failed, otherwise `null`
- `files`: Array of pasted files

#### `smartPunctuation?: 'default' | 'enable' | 'disable'` (iOS only)
Controls iOS smart punctuation behavior.

**Default:** `'default'`

### Types

```typescript
interface PastedFile {
fileName: string;
fileSize: number;
type: string; // MIME type
uri: string; // file:// URI
}

type PasteInputRef = TextInput; // Fully compatible with TextInput ref
```

## Architecture

This library uses a hybrid approach to provide paste interception across platforms:

### iOS
- Uses a **TurboModule** with dynamic subclassing (ISA swizzling)
- Wraps standard React Native `TextInput` (100% compatible)
- Registers the TextInput ref with the native module on mount
- Intercepts paste events at the UIKit level
- Works in both bridgeless and bridge modes

### Android
- Uses a custom **ComponentView** that extends `ReactEditText`
- Overrides `onCreateInputConnection` to intercept paste via `InputConnectionCompat`
- Handles clipboard content from various sources (images, files, Google Docs, etc.)
- Fully compatible with TextInput API

## Compatibility

| React Native Version | Supported |
|---------------------|-----------|
| 0.83.x | ✅ |
| 0.82.x | ✅ |
| 0.81.x | ✅ |
| 0.80.x | ✅ |
| 0.79.x | ✅ |
| 0.78.x | ✅ |
| 0.77.x | ✅ |
| 0.76.x | ✅ |
| < 0.76 | ❌ (Old Architecture not supported) |

## Troubleshooting

### iOS: Views not being registered

If you see errors about views not being found:
1. Ensure you've called `PasteInputModule.setup` in your AppDelegate (see iOS Setup above)
2. Make sure you've run `pod install` after adding the library
3. Clean build folder and rebuild: `cd ios && rm -rf build && cd .. && npx react-native run-ios`

### Android: Build errors

If you encounter build errors:
1. Clean the build: `cd android && ./gradlew clean && cd ..`
2. Rebuild: `npx react-native run-android`

## Contributing

Expand Down
45 changes: 43 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'

android {
compileSdkVersion safeExtGet('compileSdkVersion', 33)
compileSdkVersion safeExtGet('compileSdkVersion', 36)
namespace "com.mattermost.pasteinputtext"

defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 24)
targetSdkVersion safeExtGet('targetSdkVersion', 33)
targetSdkVersion safeExtGet('targetSdkVersion', 36)
buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true")
}

Expand All @@ -55,3 +55,44 @@ repositories {
dependencies {
implementation 'com.facebook.react:react-native:+'
}

if (isNewArchitectureEnabled()) {
// The codegen generates a thin typedef for PasteTextInputShadowNode that lacks
// MeasurableYogaNode, so Yoga never calls measureContent() and multiline auto-resize
// never works. After codegen runs we overwrite the generated files with our versions
// from src/codegen-patch/, which wire up the full AndroidTextInput shadow node
// machinery (MeasurableYogaNode traits + TextLayoutManager injection via adopt()).
afterEvaluate {
def patchTask = tasks.register("patchPasteTextInputCodegen") {
def patchSrcDir = file("src/codegen-patch/react/renderer/components/PasteTextInputSpecs")
def codegenDir = layout.buildDirectory.dir(
"generated/source/codegen/jni/react/renderer/components/PasteTextInputSpecs"
).get().asFile

inputs.dir(patchSrcDir)
inputs.dir(codegenDir).optional()
outputs.upToDateWhen { false }

doLast {
if (!codegenDir.exists()) {
logger.warn("[PasteInput] codegen dir not found – skipping patch")
return
}

["ShadowNodes.h", "ShadowNodes.cpp", "ComponentDescriptors.h", "ComponentDescriptors.cpp"].each { name ->
def src = new File(patchSrcDir, name)
def dst = new File(codegenDir, name)
dst.text = src.text
}

logger.lifecycle("[PasteInput] Patched ShadowNodes.h/.cpp and ComponentDescriptors.h/.cpp")
}
}

tasks.configureEach { task ->
if (task.name == "generateCodegenArtifactsFromSchema") {
task.finalizedBy(patchTask)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#include "ComponentDescriptors.h"

namespace facebook::react {

void PasteTextInputSpecs_registerComponentDescriptorsFromCodegen(
std::shared_ptr<const ComponentDescriptorProviderRegistry> registry) {
registry->add(
concreteComponentDescriptorProvider<PasteTextInputComponentDescriptor>());
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#include <react/renderer/components/PasteTextInputSpecs/ShadowNodes.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/textlayoutmanager/TextLayoutManager.h>

namespace facebook::react {

// Full class definition must live in the header because autolinking.cpp passes
// this type to concreteComponentDescriptorProvider<T>(), which requires a
// complete type. The adopt() override injects TextLayoutManager so that
// measureContent() can perform text measurement for multiline auto-resize.
class PasteTextInputComponentDescriptor final
: public ConcreteComponentDescriptor<PasteTextInputShadowNode> {
public:
PasteTextInputComponentDescriptor(const ComponentDescriptorParameters& parameters)
: ConcreteComponentDescriptor<PasteTextInputShadowNode>(parameters),
textLayoutManager_(
std::make_shared<TextLayoutManager>(contextContainer_)) {}

protected:
void adopt(ShadowNode& shadowNode) const override {
auto& textInputShadowNode =
static_cast<PasteTextInputShadowNode&>(shadowNode);
textInputShadowNode.setTextLayoutManager(textLayoutManager_);
textInputShadowNode.dirtyLayout();
ConcreteComponentDescriptor::adopt(shadowNode);
}

private:
std::shared_ptr<const TextLayoutManager> textLayoutManager_;
};

void PasteTextInputSpecs_registerComponentDescriptorsFromCodegen(
std::shared_ptr<const ComponentDescriptorProviderRegistry> registry);

} // namespace facebook::react
Loading