Skip to content

feat: drop old arch, upgrade to RN 0.83#52

Open
enahum wants to merge 16 commits intomasterfrom
rn83
Open

feat: drop old arch, upgrade to RN 0.83#52
enahum wants to merge 16 commits intomasterfrom
rn83

Conversation

@enahum
Copy link
Copy Markdown
Contributor

@enahum enahum commented May 6, 2026

⚠️ Breaking Change

This release drops support for the React Native old architecture (Bridge + Paper renderer). The library now requires new architecture (Fabric + TurboModules) to be enabled.

Summary

A major simplification that fully embraces React Native's new architecture, upgrading the library to RN 0.83.

iOS

  • Replaced the legacy PasteTextInput.mm view manager + custom Fabric spec files with a thin PasteInputModule.mm TurboModule and a PasteTextInputComponentView that delegates everything to RN's native RCTTextInputComponentView
  • Removed all manually-authored Fabric spec files (ios/PasteTextInputSpecs/) — codegen now handles them
  • Fixed smart punctuation passthrough and regular text paste

Android

  • Fixed multiline auto-resize on Fabric: codegen generates a plain ConcreteViewShadowNode typedef for PasteTextInput that lacks MeasurableYogaNode, so Yoga never called measureContent(). A Gradle task (patchPasteTextInputCodegen) now overwrites the generated files after codegen runs with versions from android/src/codegen-patch/ that wire up the full AndroidTextInput shadow node machinery (MeasurableYogaNode + LeafYogaNode + BaselineYogaNode traits, TextLayoutManager injection via adopt())
  • Added PasteInputModule TurboModule stub to satisfy codegen requirements

General

  • Upgraded to React Native 0.83
  • Removed all old architecture code paths — new arch only

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

Replaces legacy iOS view manager and generated Fabric artifacts with a Fabric-first implementation: adds an iOS TurboModule that dynamically subclasses text inputs to intercept paste, introduces iOS Fabric component stubs, adds Android codegen patches and a TurboModule stub, ships a cross-platform JS PasteInput surface, and updates build/configs and examples. (47 words)

Changes

Fabric-first migration, TurboModule wiring, JS surface, and build updates

Layer / File(s) Summary
Data Shape & Types
src/types.ts, src/PasteTextInputNativeComponent.ts, src/NativePasteInputModule.ts
Adds HostInstance, SmartPunctuation, PasteInputConfig; introduces TargetedEvent; updates native event payload types (adds eventCount, changes contentSize to Double), removes native selection prop; defines TurboModule spec PasteInputModule.
Core JS implementation
src/PasteInput.tsx, src/PasteTextInput.tsx, src/index.ts
New cross-platform PasteInput component (iOS: TextInput + TurboModule registration & NativeEventEmitter; Android: native PasteTextInput component); refactors PasteTextInput ref/selection handling, event typings, style overrides, and Android autoComplete mapping; index now re-exports ./PasteInput and the new type.
iOS native core (new)
ios/PasteInputModule.h, ios/PasteInputModule.mm, ios/PasteTextInputComponentView.h, ios/PasteTextInputComponentView.mm
Adds TurboModule PasteInputModule with per-instance dynamic subclassing/swizzling to intercept paste and emit onPaste; adds Fabric stub ComponentView implementing componentDescriptor provider and returning the component class.
iOS native core (removed)
ios/PasteInputView.*, ios/PasteInputTextView.*, ios/PasteTextInput.*, ios/PasteTextInputManager.mm, ios/PasteTextInputSpecs/*
Removes legacy iOS view manager, view/auxiliary classes, and many codegen-generated Specs/Props/ShadowNodes/EventEmitters/ComponentDescriptors/States/RCT helpers.
Android native core & codegen patch
android/src/codegen-patch/.../PasteTextInputSpecs/*, android/src/main/java/.../PasteInputModule.kt, android/src/main/java/.../PasteTextInputManager.kt, android/build.gradle
Adds C++ codegen-patch for PasteTextInput (ComponentDescriptor and ShadowNode impl), Kotlin TurboModule stub PasteInputModule, updates manager event map return type, and wires patchPasteTextInputCodegen Gradle task to copy patched artifacts after codegen.
Build, packaging & toolchain
package.json, react-native-paste-input.podspec, tsconfig.json, tsconfig.build.json, eslint.config.js, .eslintrc.js (deleted), .gitignore, android/build.gradle
Bumps package metadata and codegen config; Podspec iOS min updated to 15.1 and enforces RCT_NEW_ARCH_ENABLED; adds tsconfig.build.json; replaces .eslintrc.js with eslint.config.js; updates Android compile/target SDK and adds codegen patch wiring.
Example app & dev tooling
example/* (AppDelegate.swift, MainApplication.kt, metro.config.js, example package/babel/gradle files, Gemfile, etc.)
Migrates example iOS app to Swift AppDelegate and new-arch wiring; simplifies Android host init; upgrades example deps (React, RN), Metro to TypeScript-aware resolver with blockList, updates Gradle wrapper and related example build settings; README expanded with iOS integration samples.
Documentation & examples
README.md, example/src/App.tsx, example/src/Details.tsx
Extensive README rewrite with iOS integration examples and usage changes (onPaste error guard, autoComplete prop); example app UI refactor (SafeArea/keyboard wrappers) and type updates in example components.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant JS as PasteInput (JS)
  participant TM as PasteInputModule (TurboModule)
  participant NativeV as Native Text View / Component
  participant PB as Pasteboard / Filesystem

  User->>NativeV: paste action (Cmd/Ctrl+V)
  NativeV->>TM: swizzled paste intercepted
  TM->>PB: inspect pasteboard for files/data
  TM-->>JS: emit onPaste(nativeID, files[], error?)
  JS->>User: app-level onPaste handler invoked
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rn83

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (8)
example/android/app/build.gradle (1)

65-73: ⚡ Quick win

Pin jscFlavor to a specific version instead of using a wildcard.

The current dynamic version 2026004.+ makes builds non-reproducible. Multiple versions are available (e.g., 2026004.0.0, 2026004.0.1), and the build could pull a different JSC binary on subsequent dependency refreshes. Pin to a specific version like 2026004.0.1 and update it intentionally when needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@example/android/app/build.gradle` around lines 65 - 73, The jscFlavor
dependency is currently using a dynamic version string
('io.github.react-native-community:jsc-android-intl:2026004.+') which makes
builds non-reproducible; replace the wildcard with a pinned, explicit version
(e.g., 'io.github.react-native-community:jsc-android-intl:2026004.0.1') by
updating the def jscFlavor assignment so future builds always resolve the same
JSC artifact.
ios/PasteInputModule.mm (1)

110-114: 💤 Low value

Magic numbers for retry logic.

The retry count (10) and delay (50ms) are hard-coded. Consider extracting these as named constants for clarity and easier tuning.

♻️ Extract constants
+static const NSInteger kMaxRetryCount = 10;
+static const NSTimeInterval kRetryDelaySeconds = 0.05;
+
 `@implementation` PasteInputModule
 ...
-        } else if (retryCount < 10) {
+        } else if (retryCount < kMaxRetryCount) {
             // Retry after a short delay (view might not be mounted yet)
-            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kRetryDelaySeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/PasteInputModule.mm` around lines 110 - 114, The retry loop in
findAndRegisterViewWithTag uses hard-coded literals (10 and 0.05); extract them
into named constants (e.g., kMaxRetryCount and kRetryDelaySeconds) declared near
the top of the implementation or file scope and replace the literals in the
method and dispatch_after call with those constants so the retry limit and delay
are self-documenting and easy to tune.
ios/PasteInputModule.h (1)

24-28: 💤 Low value

Old architecture fallback may be dead code.

The PR objectives state this release drops support for the old architecture and is new-architecture-only. However, this header still includes a fallback path for RCTBridgeModule when RCT_NEW_ARCH_ENABLED is not defined. If old architecture is truly unsupported, consider removing the fallback to avoid confusion and potential maintenance burden.

♻️ Suggested simplification if old arch is unsupported
-#ifdef RCT_NEW_ARCH_ENABLED
 `@interface` PasteInputModule : RCTEventEmitter <NativePasteInputModuleSpec>
-#else
-@interface PasteInputModule : RCTEventEmitter <RCTBridgeModule>
-#endif
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/PasteInputModule.h` around lines 24 - 28, The header still contains an
old-architecture fallback; remove the `#ifdef/`#else/#endif and the
RCTBridgeModule branch so the interface only declares PasteInputModule :
RCTEventEmitter <NativePasteInputModuleSpec> (i.e. assume RCT_NEW_ARCH_ENABLED
always true). Update or remove the RCT_NEW_ARCH_ENABLED preprocessor usage and
any references to RCTBridgeModule in this file to keep the header new-arch-only
and avoid dead code.
README.md (1)

105-108: 💤 Low value

Minor: Add blank lines around table for markdown lint compliance.

The markdownlint tool flags that tables should be surrounded by blank lines (MD058).

♻️ Add blank lines around table
 ## Demo
+
 | Android | iOS |
 |---------|-----|
 |![Android](/example/gifs/AndroidPasteInput.gif)|![iOS](/example/gifs/iOSPasteInput.gif)|
+
 ## Usage
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 105 - 108, The Markdown table under the "## Demo"
heading violates MD058 because it isn't surrounded by blank lines; update the
README.md by inserting a single blank line between the "## Demo" heading and the
table start and another blank line after the table end (the rows containing |
Android | iOS | and the GIF cell row) so the table is separated by blank lines
and linter MD058 is satisfied.
example/metro.config.js (1)

29-40: 💤 Low value

Silent fallback when source file not found.

If none of the extension variants exist, the resolver falls back to base (the path without extension). This could lead to confusing errors at runtime. Consider logging a warning or throwing an explicit error if the source file cannot be located.

♻️ Optional: Add explicit handling when file not found
         resolveRequest: (context, moduleName, platform) => {
             if (moduleName === pak.name) {
                 const base = path.join(root, pak.source);
                 const extensions = ['.tsx', '.ts', '.jsx', '.js'];
                 const filePath =
                     extensions
                         .map((ext) => base + ext)
-                        .find((f) => fs.existsSync(f)) ?? base;
+                        .find((f) => fs.existsSync(f));
+                if (!filePath) {
+                    console.warn(`[metro] Could not find source for ${pak.name} at ${base}`);
+                    return context.resolveRequest(context, moduleName, platform);
+                }
                 return { filePath, type: 'sourceFile' };
             }
             return context.resolveRequest(context, moduleName, platform);
         },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@example/metro.config.js` around lines 29 - 40, The custom resolver in
resolveRequest returns base when no extension matches, which hides a missing
source file; update the logic in resolveRequest (the block using pak, base,
extensions, filePath) to explicitly handle the "not found" case: after computing
filePath, check whether the resolved file actually exists (or whether the
extensions.find returned a value) and if not either log a clear warning/error
(via console.warn or your logger) or throw a descriptive Error indicating the
package source file for pak.name could not be located, instead of silently
returning base; keep the existing fallback to context.resolveRequest for
unrelated modules.
android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt (1)

63-63: 💤 Low value

Unused constant TAG.

The TAG constant is defined but not used anywhere in this class. Consider removing it or using it for logging if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt`
at line 63, The constant TAG in PasteTextInputManager.kt is unused; either
remove the declaration of TAG or use it for logging calls within the
PasteTextInputManager class (e.g., in methods like onCreateInputConnection or
any exception/flow logs) so it is referenced; update imports if removing or add
log statements using TAG with Android Log (Log.d/e) where appropriate to keep
lint clean.
eslint.config.js (1)

23-27: ⚡ Quick win

React settings declared but plugin not configured.

The settings.react block specifies version: 'detect', but eslint-plugin-react is not imported or added to the plugins. This setting has no effect without the React plugin. Either add the React plugin or remove these unused settings.

♻️ Option A: Remove unused React settings
         languageOptions: {
             parser: tsparser,
             globals: {
                 __DEV__: true,
             },
         },
-        settings: {
-            react: {
-                version: 'detect',
-            },
-        },
         rules: {
♻️ Option B: Add eslint-plugin-react if React linting is desired
 const tseslint = require('@typescript-eslint/eslint-plugin');
 const tsparser = require('@typescript-eslint/parser');
 const prettierConfig = require('eslint-config-prettier');
 const prettierPlugin = require('eslint-plugin-prettier');
+const reactPlugin = require('eslint-plugin-react');

 /** `@type` {import('eslint').Linter.Config[]} */
 module.exports = [
     {
         ignores: ['node_modules/**', 'lib/**'],
     },
     {
         files: ['**/*.{js,ts,tsx}'],
         plugins: {
             '@typescript-eslint': tseslint,
             'prettier': prettierPlugin,
+            'react': reactPlugin,
         },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@eslint.config.js` around lines 23 - 27, The settings.react.version = 'detect'
entry is ineffective without the React plugin; either remove the settings.react
block from eslint.config.js to drop unused config, or install and enable
eslint-plugin-react and add 'react' to the plugins/extends so the setting is
used (e.g., add the plugin name to the plugins array and include a recommended
config in extends). Ensure the change targets the settings.react.version symbol
and the plugin name eslint-plugin-react so the linter recognizes React-specific
rules.
src/PasteTextInput.tsx (1)

32-51: 💤 Low value

Minor: useMergeRefs dependency array always creates new reference.

The [...refs] spread in the dependency array creates a new array on every call, which means useCallback will always return a new function. Since refs is already a rest parameter (which is an array), the spread is unnecessary. However, given that the call site passes individual arguments that form a new array each render anyway, this is a negligible inefficiency.

Optional simplification
     return React.useCallback(
         (current: Instance | null) => {
             for (const ref of refs) {
                 // ...
             }
         },
-        [...refs]
+        refs
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/PasteTextInput.tsx` around lines 32 - 51, The dependency array for
useCallback in useMergeRefs unnecessarily uses [...refs], creating a new array
each render; change the dependency to use refs directly (i.e., replace [...refs]
with refs) so the callback doesn't recreate due to the extra spread allocation
and to match the rest parameter named refs used in the function signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@example/android/gradle/wrapper/gradle-wrapper.properties`:
- Line 3: The Gradle wrapper distributionUrl currently points to gradle-9.0.0
which is unsupported by current AGP; update the distributionUrl in
gradle-wrapper.properties to gradle-9.1.0 (or later) and commit the change, and
then open both example/android/build.gradle and android/build.gradle and replace
the unpinned classpath("com.android.tools.build:gradle") entries with explicit
AGP coordinates (e.g., com.android.tools.build:gradle:9.0.0 or a compatible 9.x
pinned version) ensuring any legacy AGP 7.3.1 usages are upgraded to a 9.x AGP
that matches the new Gradle wrapper.

In `@example/android/gradlew.bat`:
- Around line 1-4: Remove the redundant MIT header comments at the top of the
gradlew.bat wrapper script (the leading `@REM` lines that duplicate license
information) so the file only contains the intended Apache-2.0 wrapper header
later in the file; delete the extra top-level MIT `@REM` comment block and leave
the existing Apache-2.0 notice intact to avoid licensing ambiguity for
downstream consumers.

In `@example/ios/.xcode.env.local`:
- Line 1: The NODE_BINARY variable in .xcode.env.local is hard-coded to a
machine-specific path; replace it with a portable reference (e.g., use `node`
from PATH or a nodenv/nvm shim) so other contributors and CI can resolve Node
without that absolute path—update the export of NODE_BINARY in the file to point
to a portable binary name or an environment-derived path (refer to the
NODE_BINARY variable in this file) and ensure any build scripts use that
portable value.

In `@ios/PasteInputModule.mm`:
- Around line 318-321: The association of the module using
objc_setAssociatedObject(view, kPasteInputModuleKey, self,
OBJC_ASSOCIATION_ASSIGN) risks a dangling pointer; change the association to use
OBJC_ASSOCIATION_RETAIN_NONATOMIC (or store a small weak-wrapper object) so the
PasteInputModule referenced by kPasteInputModuleKey is retained while views are
registered, and ensure pasteInputInterceptedPasteIMP reads the retained
reference; alternatively, implement cleanup in PasteInputModule's dealloc to
remove kPasteInputModuleKey from registered views if you prefer non-retaining
associations.
- Around line 389-396: The fallback currently constructs struct objc_super with
originalClass without guarding for originalClass == nil; change it to compute a
safe super class first (e.g., use originalClass if non-nil, otherwise
class_getSuperclass(object_getClass(self))) and assign that to
superData.super_class before calling objc_msgSendSuper; if that computed super
is still nil, call objc_msgSend(self, _cmd, sender) as a final fallback. Update
the block that builds struct objc_super and the objc_msgSendSuper call (symbols:
originalClass, struct objc_super superData, objc_msgSendSuper,
class_getSuperclass, object_getClass, objc_msgSend) so you never pass a nil
super_class to objc_msgSendSuper.

In `@package.json`:
- Around line 125-127: The Android codegen package name in package.json (the
"android"."javaPackageName" value) is incorrect
("com.mattermost.pastetextinput") and must match the actual Android source
package ("com.mattermost.pasteinputtext"); update the "javaPackageName" value to
"com.mattermost.pasteinputtext" so generated classes are emitted into the same
namespace as your native implementation.
- Line 33: The prepare script uses POSIX-only shell ops ("bob build && (test -d
.git && husky || true)"), which breaks on Windows; replace that inline shell
logic with a small Node helper and call it from the "prepare" script: keep "bob
build" as-is, then invoke a new Node script (e.g., scripts/check-husky.js) that
detects whether .git exists and runs the husky install command via a
cross-platform child_process spawn, or simply skips it when not present; update
the "prepare" entry (symbol: "prepare") to run "bob build && node
./scripts/check-husky.js" and implement the Node script to perform the
conditional logic so the behavior is identical cross-platform.

In `@react-native-paste-input.podspec`:
- Around line 19-20: The podspec currently calls install_modules_dependencies(s)
unconditionally; change it to fail fast when the old RN architecture is still
enabled by checking the standard new-architecture flag (e.g.
ENV['RCT_NEW_ARCH_ENABLED'] or the project Pod variable used in your repo)
before calling install_modules_dependencies(s), and if the flag is not set to
'1' raise/abort with a clear CocoaPods-friendly error message explaining that
the old architecture is unsupported so pod install stops immediately; place this
guard immediately before the install_modules_dependencies(s) call and keep the
error text actionable.

In `@src/PasteInput.tsx`:
- Around line 73-115: The effect currently captures the onPaste prop once at
mount causing stale callbacks; create a mutable ref (e.g., latestOnPasteRef) and
update latestOnPasteRef.current = onPaste in the component body, then change the
PasteInputEventEmitter listener inside the useEffect to call
latestOnPasteRef.current(...) instead of onPaste; keep the useEffect dependency
array empty so registration/unregistration with NativePasteInputModule
(registerTextInput/unregisterTextInput) and subscription lifecycle remain
unchanged but ensure you reference nativeIDRef/current as now done.

In `@src/types.ts`:
- Line 60: The onPaste callback in the interface is declared required but
consumers guard for undefined; update the type of the onPaste member (the
onPaste symbol in the interface in src/types.ts) to be optional by adding the
optional marker and keeping the same signature (error: string | null |
undefined, files: Array<PastedFile>): void so implementations like PasteInput
and PasteTextInput no longer conflict with the type system.

---

Nitpick comments:
In
`@android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt`:
- Line 63: The constant TAG in PasteTextInputManager.kt is unused; either remove
the declaration of TAG or use it for logging calls within the
PasteTextInputManager class (e.g., in methods like onCreateInputConnection or
any exception/flow logs) so it is referenced; update imports if removing or add
log statements using TAG with Android Log (Log.d/e) where appropriate to keep
lint clean.

In `@eslint.config.js`:
- Around line 23-27: The settings.react.version = 'detect' entry is ineffective
without the React plugin; either remove the settings.react block from
eslint.config.js to drop unused config, or install and enable
eslint-plugin-react and add 'react' to the plugins/extends so the setting is
used (e.g., add the plugin name to the plugins array and include a recommended
config in extends). Ensure the change targets the settings.react.version symbol
and the plugin name eslint-plugin-react so the linter recognizes React-specific
rules.

In `@example/android/app/build.gradle`:
- Around line 65-73: The jscFlavor dependency is currently using a dynamic
version string ('io.github.react-native-community:jsc-android-intl:2026004.+')
which makes builds non-reproducible; replace the wildcard with a pinned,
explicit version (e.g.,
'io.github.react-native-community:jsc-android-intl:2026004.0.1') by updating the
def jscFlavor assignment so future builds always resolve the same JSC artifact.

In `@example/metro.config.js`:
- Around line 29-40: The custom resolver in resolveRequest returns base when no
extension matches, which hides a missing source file; update the logic in
resolveRequest (the block using pak, base, extensions, filePath) to explicitly
handle the "not found" case: after computing filePath, check whether the
resolved file actually exists (or whether the extensions.find returned a value)
and if not either log a clear warning/error (via console.warn or your logger) or
throw a descriptive Error indicating the package source file for pak.name could
not be located, instead of silently returning base; keep the existing fallback
to context.resolveRequest for unrelated modules.

In `@ios/PasteInputModule.h`:
- Around line 24-28: The header still contains an old-architecture fallback;
remove the `#ifdef/`#else/#endif and the RCTBridgeModule branch so the interface
only declares PasteInputModule : RCTEventEmitter <NativePasteInputModuleSpec>
(i.e. assume RCT_NEW_ARCH_ENABLED always true). Update or remove the
RCT_NEW_ARCH_ENABLED preprocessor usage and any references to RCTBridgeModule in
this file to keep the header new-arch-only and avoid dead code.

In `@ios/PasteInputModule.mm`:
- Around line 110-114: The retry loop in findAndRegisterViewWithTag uses
hard-coded literals (10 and 0.05); extract them into named constants (e.g.,
kMaxRetryCount and kRetryDelaySeconds) declared near the top of the
implementation or file scope and replace the literals in the method and
dispatch_after call with those constants so the retry limit and delay are
self-documenting and easy to tune.

In `@README.md`:
- Around line 105-108: The Markdown table under the "## Demo" heading violates
MD058 because it isn't surrounded by blank lines; update the README.md by
inserting a single blank line between the "## Demo" heading and the table start
and another blank line after the table end (the rows containing | Android | iOS
| and the GIF cell row) so the table is separated by blank lines and linter
MD058 is satisfied.

In `@src/PasteTextInput.tsx`:
- Around line 32-51: The dependency array for useCallback in useMergeRefs
unnecessarily uses [...refs], creating a new array each render; change the
dependency to use refs directly (i.e., replace [...refs] with refs) so the
callback doesn't recreate due to the extra spread allocation and to match the
rest parameter named refs used in the function signature.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 999be62f-27e8-488d-a193-6c5e7298a308

📥 Commits

Reviewing files that changed from the base of the PR and between f260447 and ea0ea2c.

⛔ Files ignored due to path filters (5)
  • example/Gemfile.lock is excluded by !**/*.lock
  • example/android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
  • example/ios/Podfile.lock is excluded by !**/*.lock
  • example/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (67)
  • .eslintrc.js
  • .gitignore
  • README.md
  • android/build.gradle
  • android/src/codegen-patch/react/renderer/components/PasteTextInputSpecs/ComponentDescriptors.cpp
  • android/src/codegen-patch/react/renderer/components/PasteTextInputSpecs/ComponentDescriptors.h
  • android/src/codegen-patch/react/renderer/components/PasteTextInputSpecs/ShadowNodes.cpp
  • android/src/codegen-patch/react/renderer/components/PasteTextInputSpecs/ShadowNodes.h
  • android/src/main/java/com/mattermost/pasteinputtext/PasteInputModule.kt
  • android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt
  • android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputPackage.kt
  • eslint.config.js
  • example/Gemfile
  • example/android/app/build.gradle
  • example/android/app/src/main/java/com/example/mattermostreactnativepasteinput/MainApplication.kt
  • example/android/build.gradle
  • example/android/gradle.properties
  • example/android/gradle/wrapper/gradle-wrapper.properties
  • example/android/gradlew
  • example/android/gradlew.bat
  • example/babel.config.js
  • example/ios/.xcode.env.local
  • example/ios/Podfile
  • example/ios/ReactNativePasteInputExample-Bridging-Header.h
  • example/ios/ReactNativePasteInputExample.xcodeproj/project.pbxproj
  • example/ios/ReactNativePasteInputExample/AppDelegate.h
  • example/ios/ReactNativePasteInputExample/AppDelegate.mm
  • example/ios/ReactNativePasteInputExample/AppDelegate.swift
  • example/ios/ReactNativePasteInputExample/Info.plist
  • example/ios/ReactNativePasteInputExample/main.m
  • example/metro.config.js
  • example/package.json
  • example/src/App.tsx
  • example/src/Details.tsx
  • ios/PasteInput-Bridging-Header.h
  • ios/PasteInputModule.h
  • ios/PasteInputModule.mm
  • ios/PasteInputTextView.h
  • ios/PasteInputTextView.m
  • ios/PasteInputView.h
  • ios/PasteInputView.m
  • ios/PasteTextInput.h
  • ios/PasteTextInput.mm
  • ios/PasteTextInputComponentView.h
  • ios/PasteTextInputComponentView.mm
  • ios/PasteTextInputManager.mm
  • ios/PasteTextInputSpecs/ComponentDescriptors.cpp
  • ios/PasteTextInputSpecs/ComponentDescriptors.h
  • ios/PasteTextInputSpecs/EventEmitters.cpp
  • ios/PasteTextInputSpecs/EventEmitters.h
  • ios/PasteTextInputSpecs/Props.cpp
  • ios/PasteTextInputSpecs/Props.h
  • ios/PasteTextInputSpecs/RCTComponentViewHelpers.h
  • ios/PasteTextInputSpecs/ShadowNodes.cpp
  • ios/PasteTextInputSpecs/ShadowNodes.h
  • ios/PasteTextInputSpecs/States.cpp
  • ios/PasteTextInputSpecs/States.h
  • package.json
  • react-native-paste-input.podspec
  • src/NativePasteInputModule.ts
  • src/PasteInput.tsx
  • src/PasteTextInput.tsx
  • src/PasteTextInputNativeComponent.ts
  • src/index.ts
  • src/types.ts
  • tsconfig.build.json
  • tsconfig.json
💤 Files with no reviewable changes (24)
  • ios/PasteInput-Bridging-Header.h
  • example/ios/ReactNativePasteInputExample/AppDelegate.mm
  • ios/PasteInputView.h
  • ios/PasteInputTextView.h
  • example/ios/ReactNativePasteInputExample-Bridging-Header.h
  • ios/PasteTextInput.mm
  • ios/PasteInputView.m
  • ios/PasteTextInputSpecs/ComponentDescriptors.h
  • ios/PasteInputTextView.m
  • ios/PasteTextInputSpecs/RCTComponentViewHelpers.h
  • ios/PasteTextInputManager.mm
  • example/ios/ReactNativePasteInputExample/main.m
  • example/ios/ReactNativePasteInputExample/AppDelegate.h
  • ios/PasteTextInput.h
  • ios/PasteTextInputSpecs/Props.h
  • ios/PasteTextInputSpecs/States.h
  • .eslintrc.js
  • ios/PasteTextInputSpecs/ShadowNodes.cpp
  • ios/PasteTextInputSpecs/States.cpp
  • ios/PasteTextInputSpecs/EventEmitters.h
  • ios/PasteTextInputSpecs/Props.cpp
  • ios/PasteTextInputSpecs/EventEmitters.cpp
  • ios/PasteTextInputSpecs/ShadowNodes.h
  • ios/PasteTextInputSpecs/ComponentDescriptors.cpp

Comment thread example/android/gradle/wrapper/gradle-wrapper.properties
Comment thread example/android/gradlew.bat
Comment thread example/ios/.xcode.env.local Outdated
Comment thread ios/PasteInputModule.mm
Comment thread ios/PasteInputModule.mm Outdated
Comment thread package.json Outdated
Comment thread package.json
Comment thread react-native-paste-input.podspec
Comment thread src/PasteInput.tsx
Comment thread src/types.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ios/PasteInputModule.mm`:
- Around line 96-115: The retry chain in
findAndRegisterViewWithTag:nativeID:config:retryCount: must be
cancellable/versioned per nativeID so stale retries don't
re-register/unnecessarily re-subclass views; add a per-nativeID token or
incrementing version stored on the instance when registerTextInput: is called
(and cleared/updated in unregisterTextInput:) and check that token/version both
before scheduling each dispatch_after and immediately before calling
applyDynamicSubclassing: and writing to self.registeredViews; if the
token/version has changed or the nativeID is unregistered, abort the retry and
do not call applyDynamicSubclassing:.
- Around line 511-520: The paste handling currently returns YES whenever the
pasteboard has content, bypassing superclass checks; update the paste branch
(where action == `@selector`(paste:)) to first call the superclass check (e.g.,
BOOL superAllowed = [super canPerformAction:action withSender:sender] or the
appropriate superclass method) and only return YES if superAllowed is true and
the pasteboard has content; otherwise return the superclass result so
read-only/disabled controls don't show Paste.
- Around line 141-144: The removeListeners: implementation in PasteInputModule
is missing the matching super call, causing RCTEventEmitter listener accounting
to be unbalanced; update the - (void)removeListeners:(double)count method to
call [super removeListeners:count] (matching the addListener: use of [super
addListener:eventName]) so the emitter's listener count is decremented correctly
when JavaScript removes subscriptions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 134b957a-4b31-4fc7-b2b8-6f656fb18fac

📥 Commits

Reviewing files that changed from the base of the PR and between ea0ea2c and de8e280.

📒 Files selected for processing (3)
  • .gitignore
  • example/ios/.xcode.env.local
  • ios/PasteInputModule.mm
💤 Files with no reviewable changes (1)
  • example/ios/.xcode.env.local

Comment thread ios/PasteInputModule.mm Outdated
Comment thread ios/PasteInputModule.mm
Comment thread ios/PasteInputModule.mm
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
package.json (1)

33-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep prepare git-aware.

Line 33 now runs husky unconditionally. That still breaks prepare in non-repo contexts, so installs or release packaging can fail with a missing .git checkout. Please gate Husky behind a small Node helper and no-op when .git is absent.

Suggested direction
-    "prepare": "bob build && husky",
+    "prepare": "bob build && node ./scripts/prepare-husky.js",
Does npm run the `prepare` script for git dependencies and packaging flows, and does `husky` fail when the working directory does not contain a `.git` directory?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 33, Update the prepare step to be git-aware: create a
small Node helper (e.g., scripts/prepare-husky.js) that checks for the existence
of a .git directory and only runs the husky installer when .git is present,
otherwise no-ops; then change package.json's "prepare" to call this helper
(replacing the unconditional "husky" invocation) so installs and packaging
succeed in non-repo contexts. Ensure the helper uses fs.existsSync or equivalent
to detect ".git" and spawns/executes the husky command when appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ios/PasteInputModule.mm`:
- Around line 286-317: applySmartPunctuationSettings: mutates
UITextView/UITextField smartQuotesType/smartDashesType but never records
original values, so recycled native views can inherit wrong settings; modify
applySmartPunctuationSettings: (and the related implementations at the other
occurrences) to store the original UITextSmartQuotesType and
UITextSmartDashesType in an associated object (keyed by the view) the first time
you change them, and update removeDynamicSubclassing: (the unregister path) to
read those saved values and restore smartQuotesType and smartDashesType on the
UITextView/UITextField before removing associations; ensure the code only saves
originals once and clears the associated storage after restoring.
- Around line 343-372: The dynamic subclass name currently includes the view
pointer (NSString *dynamicClassName = [NSString
stringWithFormat:@"PasteInput_%@_%p", className, view]) which creates a new
Objective-C class per mount; change this to a single cached subclass per
original class (e.g., "PasteInput_<OriginalClass>") so NSClassFromString(...)
can find and reuse the class instead of allocating one per view, and keep
per-view state in associated objects on the view; update the logic around
objc_allocateClassPair(...) and objc_registerClassPair(...) to only
allocate/register when NSClassFromString returns nil and ensure the paste: and
canPerformAction:withSender: method overrides are added to that
per-original-class dynamic subclass.
- Around line 79-106: The registerTextInput: method mutates
self.registrationGenerations off the main thread causing a race with
findAndRegisterViewWithTag: and unregisterTextInput: which access the same
dictionaries on the main queue; to fix, move the generation bump and the call to
findAndRegisterViewWithTag: so they execute on the main thread (i.e., dispatch
the generation increment, assignment to self.registrationGenerations[nativeID],
and the subsequent call to
findAndRegisterViewWithTag:nativeID:config:retryCount:generation: onto the main
queue) so all reads/writes to registrationGenerations happen only on the main
thread and avoid concurrent access.

In `@src/PasteInput.tsx`:
- Line 29: The component is reading the private __nativeTag from the TextInput
host (in PasteInput.tsx) which breaks on Fabric; replace that usage with the
public nativeID approach: add a generated nativeID prop (e.g.,
nativeID={generateUniqueId()}) to the TextInput and change the effect/register
logic that currently reads __nativeTag to instead read the nativeID and use that
to register the iOS wrapper/handler; also make the registration effect resilient
by retrying or waiting until nativeID is present instead of silently no-op when
missing (update the effect that references __nativeTag to use nativeID and retry
logic).

---

Duplicate comments:
In `@package.json`:
- Line 33: Update the prepare step to be git-aware: create a small Node helper
(e.g., scripts/prepare-husky.js) that checks for the existence of a .git
directory and only runs the husky installer when .git is present, otherwise
no-ops; then change package.json's "prepare" to call this helper (replacing the
unconditional "husky" invocation) so installs and packaging succeed in non-repo
contexts. Ensure the helper uses fs.existsSync or equivalent to detect ".git"
and spawns/executes the husky command when appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 260a7d97-d3a2-4296-91f2-ece85470f9ed

📥 Commits

Reviewing files that changed from the base of the PR and between de8e280 and 6d3e85b.

📒 Files selected for processing (5)
  • ios/PasteInputModule.mm
  • package.json
  • react-native-paste-input.podspec
  • src/PasteInput.tsx
  • src/types.ts

Comment thread ios/PasteInputModule.mm Outdated
Comment thread ios/PasteInputModule.mm
Comment thread ios/PasteInputModule.mm Outdated
Comment thread src/PasteInput.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
ios/PasteInputModule.mm (1)

206-223: ⚡ Quick win

Add defensive respondsToSelector: checks for chained performSelector: calls.

The chained calls to mountingManager and componentViewRegistry selectors assume these APIs exist. If React Native's internal API changes, this will crash with an unrecognized selector exception. Consider adding respondsToSelector: guards for each step, consistent with the pattern used elsewhere in this file (e.g., line 184, 218).

♻️ Suggested defensive pattern
         // Get mountingManager from surfacePresenter
-        id mountingManager = [surfacePresenter performSelector:`@selector`(mountingManager)];
+        id mountingManager = nil;
+        if ([surfacePresenter respondsToSelector:`@selector`(mountingManager)]) {
+            mountingManager = [surfacePresenter performSelector:`@selector`(mountingManager)];
+        }
         if (mountingManager) {
 
             // Get componentViewRegistry from mountingManager
-            id componentViewRegistry = [mountingManager performSelector:`@selector`(componentViewRegistry)];
+            id componentViewRegistry = nil;
+            if ([mountingManager respondsToSelector:`@selector`(componentViewRegistry)]) {
+                componentViewRegistry = [mountingManager performSelector:`@selector`(componentViewRegistry)];
+            }
             if (componentViewRegistry) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/PasteInputModule.mm` around lines 206 - 223, The code calls
[surfacePresenter performSelector:`@selector`(mountingManager)] and
[mountingManager performSelector:`@selector`(componentViewRegistry)] without
checking that surfacePresenter and mountingManager respond to those selectors;
add defensive respondsToSelector: guards before each performSelector call (check
surfacePresenter respondsToSelector:`@selector`(mountingManager) and
mountingManager respondsToSelector:`@selector`(componentViewRegistry)) and only
call objc_msgSend for findComponentViewWithTag when componentViewRegistry
respondsToSelector:`@selector`(findComponentViewWithTag:), keeping the existing
use of findComponentViewWithTag, componentViewRegistry, mountingManager,
surfacePresenter, objc_msgSend and viewRef to locate and call the method safely.
ios/UIPasteboard+GetImageInfo.m (1)

37-45: 💤 Low value

Minor: consider using break instead of the added flag with continue.

The current pattern using added = YES followed by a continue check at the loop start works, but a break after successfully adding a file (line 75) would be more direct and eliminate the need for the flag check on each iteration.

That said, the existing approach is functional and consistent with the code's style.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/UIPasteboard`+GetImageInfo.m around lines 37 - 45, The loop currently
uses an "added" flag and a check at the top to skip further processing; instead,
remove the added flag and its early-check, and after successfully adding a file
(where the code sets added = YES and uses continue), replace that behavior with
a direct break to exit the loop; update any references around the branches that
call getDataForImageItem: and dataForPasteboardType: so the control flow ends
with break after successful addition and no longer relies on the added boolean.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ios/PasteInputModule.mm`:
- Around line 173-192: The getSurfacePresenter method can call
performSelector:`@selector`(surfacePresenter) on _reactHost even if it doesn't
implement that selector, causing an unrecognized selector crash; add a defensive
check similar to the bridge path: before calling performSelector on _reactHost
in getSurfacePresenter verify [_reactHost
respondsToSelector:`@selector`(surfacePresenter)] and only call performSelector
(or return the result) when true, otherwise fall through to the existing bridge
fallback or return nil.

---

Nitpick comments:
In `@ios/PasteInputModule.mm`:
- Around line 206-223: The code calls [surfacePresenter
performSelector:`@selector`(mountingManager)] and [mountingManager
performSelector:`@selector`(componentViewRegistry)] without checking that
surfacePresenter and mountingManager respond to those selectors; add defensive
respondsToSelector: guards before each performSelector call (check
surfacePresenter respondsToSelector:`@selector`(mountingManager) and
mountingManager respondsToSelector:`@selector`(componentViewRegistry)) and only
call objc_msgSend for findComponentViewWithTag when componentViewRegistry
respondsToSelector:`@selector`(findComponentViewWithTag:), keeping the existing
use of findComponentViewWithTag, componentViewRegistry, mountingManager,
surfacePresenter, objc_msgSend and viewRef to locate and call the method safely.

In `@ios/UIPasteboard`+GetImageInfo.m:
- Around line 37-45: The loop currently uses an "added" flag and a check at the
top to skip further processing; instead, remove the added flag and its
early-check, and after successfully adding a file (where the code sets added =
YES and uses continue), replace that behavior with a direct break to exit the
loop; update any references around the branches that call getDataForImageItem:
and dataForPasteboardType: so the control flow ends with break after successful
addition and no longer relies on the added boolean.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 807813d1-06f3-4d75-a6f9-114f924dc7d1

📥 Commits

Reviewing files that changed from the base of the PR and between 6d3e85b and baee13c.

📒 Files selected for processing (2)
  • ios/PasteInputModule.mm
  • ios/UIPasteboard+GetImageInfo.m

Comment thread ios/PasteInputModule.mm
@enahum
Copy link
Copy Markdown
Contributor Author

enahum commented May 6, 2026

@coderabittai resolve

@enahum enahum requested a review from larkox May 6, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant