Skip to content

fix: inverted keyboard navigation#7010

Open
OtavioStasiak wants to merge 45 commits intodevelopfrom
fix.android-inverted-keyboard-navigation
Open

fix: inverted keyboard navigation#7010
OtavioStasiak wants to merge 45 commits intodevelopfrom
fix.android-inverted-keyboard-navigation

Conversation

@OtavioStasiak
Copy link
Contributor

@OtavioStasiak OtavioStasiak commented Feb 26, 2026

Proposed changes

Issue(s)

https://rocketchat.atlassian.net/browse/MA-266

How to test or reproduce

Screenshots

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Summary by CodeRabbit

  • New Features

    • Improved keyboard navigation and focus behavior for inverted message lists and header/message composer
    • Programmatic focus control for room header and message composer
    • Accessibility labels/roles added to reaction and thread controls
  • Bug Fixes

    • Improved button disabled behavior and touch feedback consistency
  • Refactor

    • Reworked message list scroll view to support advanced keyboard/focus handling and inverter-aware navigation

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Walkthrough

Adds Android keyboard accessibility for inverted message lists: new TurboModule to track keyboard-a11y state, focus utilities and native inverted scroll views that handle inverted navigation and exit focus, MainActivity remaps keyboard events when enabled, and React components/props updated to bridge native focus behavior.

Changes

Cohort / File(s) Summary
Android A11y TurboModule
android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11ySpec.java, android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java
New TurboModule spec and implementation KeyboardA11y exposing enable(scope), disable(), and getState() with static accessors for process-wide enabled/scope state.
Main activity key remapping
android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
Overrides dispatchKeyEvent to remap TAB and DPAD navigation when KeyboardA11yModule.isEnabled() and focused view has an inverted parent.
Inverted scroll native-side
android/app/src/main/java/chat/rocket/reactnative/scroll/FocusUtils.java, .../InvertedScrollContentView.java, .../InvertedScrollContentViewManager.java, .../InvertedScrollView.java, .../InvertedScrollViewManager.java, android/app/src/main/res/values/ids.xml
Adds FocusUtils.hasInvertedParent, inverted content view with inversion flag and focus ordering, native inverted scroll view that intercepts Tab/DPAD to move focus between cells and supports exitFocusNativeId prop, and new resource id tag_inverted_list.
React Native bridge & types
app/lib/native/KeyboardInversionA11yAndroid.ts
Typed TurboModule bridge for KeyboardA11y with functions to enable/disable and query state.
Inverted scroll RN composition
app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx, app/views/RoomView/List/components/InvertedScrollView.tsx, app/views/RoomView/List/components/List.tsx
Introduces RNLikeInvertedScrollView wrapper using native inverted components, forwards exitFocusNativeId from FlatList renderScrollComponent, and replaces previous JS-only InvertedScrollView.
Header & composer focus wiring
app/containers/RoomHeader/RoomHeader.tsx, app/containers/RoomHeader/index.tsx, app/containers/MessageComposer/interfaces.ts, app/views/RoomView/index.tsx
Makes RoomHeader forward-refable with focus() via useImperativeHandle; MessageComposer ref gains focus(); RoomView listens to navigation focus and conditionally focuses header or composer and assigns nativeID to composer exit view.
Navigation param & helper
app/stacks/types.ts, app/lib/methods/helpers/goRoom.ts, app/views/RoomsListView/index.tsx
Adds optional focusHeaderOnOpen?: boolean to RoomView route params, threads option through goRoom(), and passes it from RoomsListView when opening a room.
Touch / Button gesture changes
app/containers/Touch.tsx, app/containers/Button/index.tsx, app/containers/Button/Button.test.tsx
Switches some components between Pressable and RectButton usages, updates prop typings and disabled handling, and adjusts tests/comments accordingly.
Accessibility & small UX fixes
app/containers/message/Reactions.tsx, app/containers/message/Thread.tsx, app/containers/message/Touchable.tsx, app/containers/message/index.tsx
Adds accessibility roles/labels to reaction and thread controls, replaces wrappers to use accessible touchables, introduces keyboard-focusable Pressable, and exposes onThreadPress through MessageContext.
Misc types & exports
app/views/RoomView/index.tsx, app/views/RoomView/List/*, other small files
Various prop/type additions (e.g., exitFocusNativeId, InvertedScrollViewProps), ref forwarding and re-exports to surface new focus behavior to RN layer.

Sequence Diagram

sequenceDiagram
    actor User
    participant MainActivity
    participant KeyboardA11yModule
    participant FocusUtils
    participant InvertedScrollView
    participant RoomHeader

    User->>MainActivity: Press TAB / DPAD key
    MainActivity->>KeyboardA11yModule: isEnabled()
    KeyboardA11yModule-->>MainActivity: true
    MainActivity->>FocusUtils: hasInvertedParent(focusedView)?
    FocusUtils-->>MainActivity: true
    MainActivity->>MainActivity: Remap key (TAB ↔ DPAD up/down)
    MainActivity->>InvertedScrollView: Dispatch remapped KeyEvent
    InvertedScrollView->>InvertedScrollView: Move focus between list cells
    alt reached list boundary with exitFocusNativeId
        InvertedScrollView->>RoomHeader: focus exit target (nativeID lookup)
        RoomHeader->>RoomHeader: set accessibility focus (useImperativeHandle)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: inverted keyboard navigation' accurately describes the main change, which implements keyboard navigation fixes for inverted message lists on Android.
Linked Issues check ✅ Passed All code changes address MA-266 requirements: keyboard navigation remapping in MainActivity, FocusUtils hierarchy checking, InvertedScrollView keyboard handling, RoomHeader focus support, and exit focus navigation with composer routing.
Out of Scope Changes check ✅ Passed All changes relate to keyboard navigation and focus management. Button/Touch component updates enable keyboard interaction; Message component accessibility and Thread interaction changes support keyboard navigation in list items; external keyboard dependency supports cross-platform compatibility.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.70.0.108314

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNSHg7TdML4PFPLPaD3vbMSQN1kCdTXPzueVdh4N44q6GZfR1wk0ZNlhk_GdG11POn-I-qGjSYJWmIjmQQKs

@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.70.0.108317

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNRigBMM3Bes80dsLl0Wof7_pwq8w6zFWBUUGsRGp2rRCO4FnN6K1TeJ4P-FQhppg3r60B8pyKQNdZQTh6Zy

@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.70.0.108318

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNQfUhHNuTEb7FEFekyzv7gX_VAjowLUgqhtyPgrwQ7XZI58MKTI6S0_cS2vJpxejQfVdMxEh288m2qGEJxg

@OtavioStasiak OtavioStasiak temporarily deployed to experimental_android_build March 19, 2026 18:23 — with GitHub Actions Inactive
@OtavioStasiak OtavioStasiak had a problem deploying to upload_experimental_android March 19, 2026 18:57 — with GitHub Actions Error
@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.71.0.108402

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNTYno_mVYZWLgeeLqqvSYzqCCdxONBflb9pYdCVAGkOSK_kszp7K1CMQb_acDzxkdFxSBa0aBqioyY5I5hV

@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.71.0.108405

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNTIPfz9s2BYeUgTdgwAbLARyfJ33Zu2uCC3-3khGJcUxnB3F036QKV341pD1p8E0edHZAtGH8Rj5BHqnrWZ

@OtavioStasiak
Copy link
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/RocketChat/Rocket.Chat.ReactNative/issues/comments/3967343470","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n## Walkthrough\n\nThis pull request implements keyboard navigation fixes for inverted message lists on Android by introducing keyboard accessibility state management, custom key event handling, and focus routing across inverted scroll views, with corresponding React Native prop exposure and navigation integration to ensure proper focus traversal.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Keyboard Accessibility Infrastructure** <br> `android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11ySpec.java`, `android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java`, `app/lib/native/KeyboardInversionA11yAndroid.ts`|New React Native TurboModule and TypeScript bindings for managing keyboard accessibility state (`enabled` flag and scope tracking) with `enable()`, `disable()`, and `getState()` methods.|\n|**Android Focus & Navigation Utilities** <br> `android/app/src/main/java/chat/rocket/reactnative/scroll/FocusUtils.java`, `android/app/src/main/res/values/ids.xml`|New utility class to detect inverted parent views via tag lookup; new resource ID for tagging inverted list views.|\n|**Inverted Scroll View Implementations** <br> `android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java`, `android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java`|Replaced accessibility traversal reversal with custom keyboard navigation; added `dispatchKeyEvent` override to route Tab/DPAD navigation through inverted list cells and boundary exit focus handling; introduced `setIsInvertedContent` for conditional focus behavior.|\n|**View Manager Props** <br> `android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java`, `android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java`|Exposed new React props: `exitFocusNativeId` (for boundary exit routing) and `isInvertedContent` (for conditional inverted behavior).|\n|**MainActivity Key Event Handling** <br> `android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt`|Added `dispatchKeyEvent` override to conditionally remap DPAD and Tab keys when keyboard accessibility is enabled and the focused view has an inverted parent, enabling proper directional navigation in inverted lists.|\n|**React Component Focus Integration** <br> `app/containers/RoomHeader/RoomHeader.tsx`, `app/containers/RoomHeader/index.tsx`, `app/containers/MessageComposer/interfaces.ts`|Converted `RoomHeader` to `forwardRef` with imperative `focus()` method; exposed `focus()` in `MessageComposer` ref interface; enables external focus control via `AccessibilityInfo.setAccessibilityFocus`.|\n|**Inverted Scroll View React Layer** <br> `app/views/RoomView/List/components/InvertedScrollView.tsx`, `app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx`, `app/views/RoomView/List/components/List.tsx`|Refactored inverted scroll view architecture: new `RNLikeInvertedScrollView` wrapper manages keyboard visibility and responder behavior; exposed `exitFocusNativeId` prop to route focus exit to message composer.|\n|**Navigation & Focus Routing** <br> `app/lib/methods/helpers/goRoom.ts`, `app/stacks/types.ts`, `app/views/RoomView/index.tsx`, `app/views/RoomsListView/index.tsx`|Added `focusHeaderOnOpen` navigation param and conditional header focus in `RoomView`; wrapped message composer with `nativeID='message-composer-exit-focus'` as boundary exit target.|\n|**Button Component Refactor** <br> `app/containers/Button/index.tsx`, `app/containers/Button/Button.test.tsx`|Replaced `Pressable` with `RectButton` from gesture-handler; updated disabled behavior from `disabled` prop to `enabled` inverse; configured ripple/underlay transparency.|\n\n## Sequence Diagram\n\n```mermaid\nsequenceDiagram\n    participant User as User\n    participant MainActivity as MainActivity\n    participant KeyboardA11y as KeyboardA11yModule\n    participant FocusUtils as FocusUtils\n    participant InvertedScrollView as InvertedScrollView (Android)\n    participant RoomView as RoomView (React)\n    participant RoomHeader as RoomHeader\n    participant Composer as MessageComposer\n    \n    User->>MainActivity: Press Tab/Arrow Key\n    MainActivity->>KeyboardA11y: isEnabled()?\n    KeyboardA11y-->>MainActivity: true (if enabled)\n    MainActivity->>FocusUtils: hasInvertedParent(focusedView)?\n    FocusUtils-->>MainActivity: true (if in inverted list)\n    MainActivity->>MainActivity: Remap DPAD/Tab event\n    MainActivity->>InvertedScrollView: dispatchKeyEvent(remappedEvent)\n    InvertedScrollView->>InvertedScrollView: Traverse inverted cells<br/>in visual order\n    InvertedScrollView-->>User: Focus next cell or exit\n    \n    alt Focus Exits at List Boundary\n        InvertedScrollView->>RoomView: Exit focus via nativeID<br/>message-composer-exit-focus\n        RoomView->>Composer: Focus message input\n    else Header Focus Requested\n        RoomView->>RoomHeader: ref.current.focus()\n        RoomHeader->>RoomHeader: AccessibilityInfo<br/>.setAccessibilityFocus()\n    end\n```\n\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~45 minutes\n\n## Suggested labels\n\n`type: bug`\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 3 | ❌ 2</summary>\n\n### ❌ Failed checks (1 warning, 1 inconclusive)\n\n|         Check name         | Status         | Explanation                                                                                                                                                                                                                         | Resolution                                                                                                                                                    |\n| :------------------------: | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n|     Docstring Coverage     | ⚠️ Warning     | Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%.                                                                                                                                                | Write docstrings for the functions missing them to satisfy the coverage threshold.                                                                            |\n| Out of Scope Changes check | ❓ Inconclusive | While most changes relate to keyboard navigation fixes, some modifications (Button component Pressable→RectButton swap, MessageComposer/RoomHeader ref additions) appear tangential to the core inverted keyboard navigation issue. | Clarify whether Button gesture-handler changes and ref-forwarding additions are necessary for the keyboard navigation fix or represent separate improvements. |\n\n<details>\n<summary>✅ Passed checks (3 passed)</summary>\n\n|      Check name     | Status   | Explanation                                                                                                                                                            |\n| :-----------------: | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|  Description Check  | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                            |\n|     Title check     | ✅ Passed | The PR title 'fix: inverted keyboard navigation' clearly and concisely describes the primary change addressing the inverted keyboard navigation issue on Android.      |\n| Linked Issues check | ✅ Passed | The PR implements all coding requirements from MA-266: fixes inverted message list focus navigation on Android to follow visual screen order using Tab and arrow keys. |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=RocketChat/Rocket.Chat.ReactNative&utm_content=7010)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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= -->\n\n<!-- internal state end -->"},"request":{"retryCount":3,"retries":3,"retryAfter":16}}}

Copy link
Contributor

@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: 5

🧹 Nitpick comments (2)
app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx (1)

37-37: Assigning to createRef().current is a TypeScript anti-pattern.

React.createRef() returns a RefObject with a readonly current property. While this works at runtime, it bypasses TypeScript's intent. Since you need a mutable ref, use { current: null } directly or a callback ref pattern:

♻️ Proposed fix using mutable ref object
-  private scrollRef = React.createRef<any>();
+  private scrollRef: { current: any } = { current: null };

Also applies to: 159-170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx` at line 37,
Replace the readonly RefObject created with React.createRef() with a mutable ref
object so assignments to .current are type-safe: change the declaration private
scrollRef = React.createRef<any>(); to a MutableRefObject like private
scrollRef: React.MutableRefObject<any> = { current: null }; and update the other
similar refs (the ones referenced in the later block that assign to .current) to
use the same pattern or a callback ref; keep usages that set scrollRef.current
unchanged but ensure their ref declarations are MutableRefObject types
(referencing scrollRef and the other ref identifiers in this file).
app/lib/native/KeyboardInversionA11yAndroid.ts (1)

17-19: Redundant default parameter.

Since scope is typed as literal 'room-view', the default value doesn't add flexibility—callers can only ever pass 'room-view'. Consider simplifying:

♻️ Proposed simplification
-export const enableRoomViewKeyboardA11y = (scope: 'room-view' = 'room-view') => {
-	NativeModule.enable(scope);
+export const enableRoomViewKeyboardA11y = () => {
+	NativeModule.enable('room-view');
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/native/KeyboardInversionA11yAndroid.ts` around lines 17 - 19, The
function enableRoomViewKeyboardA11y currently declares a redundant literal-typed
parameter scope with a default; remove the parameter and hardcode the literal
when calling NativeModule.enable (i.e., change enableRoomViewKeyboardA11y to
take no args and call NativeModule.enable('room-view')), keeping the function
and NativeModule.enable usage to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java`:
- Around line 30-40: The enable()/disable() methods perform two separate
volatile writes to sEnabled and sScope which can be observed in an inconsistent
intermediate state by readers such as MainActivity.dispatchKeyEvent or getState;
wrap the compound write in a single atomic operation by synchronizing or using
an atomic holder: either place both assignments inside a synchronized block
(e.g., synchronized on KeyboardA11yModule.class) in enable(String) and
disable(), or replace sEnabled/sScope with a single AtomicReference or immutable
state object (e.g., KeyboardA11yState) updated atomically; update readers
(getState, dispatchKeyEvent) to read the single synchronized/atomic state to
ensure visibility and consistency.

In `@android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt`:
- Around line 47-82: The current dispatchKeyEvent remaps only ACTION_DOWN events
causing mismatched ACTION_UP keycodes; update the condition that checks the
event action in dispatchKeyEvent (used with KeyboardA11yModule.isEnabled() and
FocusUtils.hasInvertedParent(current)) to also handle KeyEvent.ACTION_UP (e.g.,
if event.action == KeyEvent.ACTION_DOWN || event.action == KeyEvent.ACTION_UP),
perform the same keyCode mapping logic for ACTION_UP, and construct/dispatch the
inverted KeyEvent (preserving downTime, eventTime, repeatCount, metaState,
deviceId, scanCode, flags, source) so both press and release events are
consistently remapped.

In
`@android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java`:
- Line 69: In InvertedScrollView.java the code casts getChildAt(0) directly to
ViewGroup into variable contentView which can throw ClassCastException if the
child is not a ViewGroup; change this to first retrieve the child into a View,
check "instanceof ViewGroup" (and for null), then assign to contentView only
when the check passes, and handle the else case safely (e.g., return early, fall
back to a no-op, or log an error) so the view hierarchy change won't crash the
method.
- Around line 85-92: The current focus-handling in InvertedScrollView (around
variables exitDir and exitTarget) always returns true even when
findExitTarget(exitDir) returns null, which consumes the key at the list
boundary and can trap keyboard users; update the method so that when exitTarget
== null it does not consume the event—either return false to allow default focus
handling or trigger accessible feedback (e.g., call
announceForAccessibility(...) or an EdgeEffect bounce) before returning true;
locate the block using exitDir/exitTarget in InvertedScrollView and replace the
final unconditional "return true" with logic that returns false when exitTarget
is null (or invokes an accessibility feedback helper then returns true) so users
receive proper feedback or focus moves.

In `@app/containers/Button/index.tsx`:
- Around line 91-101: Add an explicit accessibilityState to the RectButton so
screen readers announce when the button is disabled: update the RectButton (in
this file's Button component) to include accessibilityState={{ disabled:
!!isDisabled }} alongside existing props (e.g., onPress, enabled={!isDisabled},
accessibilityLabel={title}) so the disabled state is conveyed to assistive tech.

---

Nitpick comments:
In `@app/lib/native/KeyboardInversionA11yAndroid.ts`:
- Around line 17-19: The function enableRoomViewKeyboardA11y currently declares
a redundant literal-typed parameter scope with a default; remove the parameter
and hardcode the literal when calling NativeModule.enable (i.e., change
enableRoomViewKeyboardA11y to take no args and call
NativeModule.enable('room-view')), keeping the function and NativeModule.enable
usage to locate the change.

In `@app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx`:
- Line 37: Replace the readonly RefObject created with React.createRef() with a
mutable ref object so assignments to .current are type-safe: change the
declaration private scrollRef = React.createRef<any>(); to a MutableRefObject
like private scrollRef: React.MutableRefObject<any> = { current: null }; and
update the other similar refs (the ones referenced in the later block that
assign to .current) to use the same pattern or a callback ref; keep usages that
set scrollRef.current unchanged but ensure their ref declarations are
MutableRefObject types (referencing scrollRef and the other ref identifiers in
this file).
🪄 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: 3ccf1ed2-867a-4803-8928-cebf49d7f3ba

📥 Commits

Reviewing files that changed from the base of the PR and between b71ee89 and 4098eed.

⛔ Files ignored due to path filters (6)
  • app/containers/Button/__snapshots__/Button.test.tsx.snap is excluded by !**/*.snap
  • app/containers/LoginServices/__snapshots__/LoginServices.test.tsx.snap is excluded by !**/*.snap
  • app/containers/UIKit/__snapshots__/UiKitMessage.test.tsx.snap is excluded by !**/*.snap
  • app/containers/UIKit/__snapshots__/UiKitModal.test.tsx.snap is excluded by !**/*.snap
  • app/containers/message/__snapshots__/Message.test.tsx.snap is excluded by !**/*.snap
  • app/views/CannedResponsesListView/__snapshots__/CannedResponseItem.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (22)
  • android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
  • android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java
  • android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11ySpec.java
  • android/app/src/main/java/chat/rocket/reactnative/scroll/FocusUtils.java
  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java
  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java
  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java
  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java
  • android/app/src/main/res/values/ids.xml
  • app/containers/Button/Button.test.tsx
  • app/containers/Button/index.tsx
  • app/containers/MessageComposer/interfaces.ts
  • app/containers/RoomHeader/RoomHeader.tsx
  • app/containers/RoomHeader/index.tsx
  • app/lib/methods/helpers/goRoom.ts
  • app/lib/native/KeyboardInversionA11yAndroid.ts
  • app/stacks/types.ts
  • app/views/RoomView/List/components/InvertedScrollView.tsx
  • app/views/RoomView/List/components/List.tsx
  • app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx
  • app/views/RoomView/index.tsx
  • app/views/RoomsListView/index.tsx
📜 Review details
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: OtavioStasiak
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6499
File: app/containers/ServerItem/index.tsx:34-36
Timestamp: 2025-12-17T15:56:22.578Z
Learning: In the Rocket.Chat React Native codebase, for radio button components on iOS, include the selection state ("Selected"/"Unselected") in the accessibilityLabel instead of using accessibilityState={{ checked: hasCheck }}, because iOS VoiceOver has known issues with accessibilityRole="radio" + accessibilityState that prevent correct state announcement.
📚 Learning: 2025-12-17T15:56:22.578Z
Learnt from: OtavioStasiak
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6499
File: app/containers/ServerItem/index.tsx:34-36
Timestamp: 2025-12-17T15:56:22.578Z
Learning: In the Rocket.Chat React Native codebase, for radio button components on iOS, include the selection state ("Selected"/"Unselected") in the accessibilityLabel instead of using accessibilityState={{ checked: hasCheck }}, because iOS VoiceOver has known issues with accessibilityRole="radio" + accessibilityState that prevent correct state announcement.

Applied to files:

  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java
  • android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java
  • app/containers/RoomHeader/RoomHeader.tsx
📚 Learning: 2026-03-04T20:13:17.288Z
Learnt from: divyanshu-patil
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6957
File: ios/RocketChat Watch App/Views/MessageComposerView.swift:37-55
Timestamp: 2026-03-04T20:13:17.288Z
Learning: In the WatchOS app (ios/RocketChat Watch App) for Rocket.Chat React Native, using SwiftUI `Button` inside a `ScrollView` on WatchOS causes accidental message sends because button tap targets can be triggered during scroll gestures. `Text` with `.onTapGesture` is the preferred pattern for tappable items in scroll views on WatchOS. To preserve accessibility, add `.accessibilityAddTraits(.isButton)` and `.accessibilityLabel()` to the `Text` element instead.

Applied to files:

  • app/containers/Button/index.tsx
  • app/containers/RoomHeader/RoomHeader.tsx
📚 Learning: 2026-03-10T15:21:45.098Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7046
File: app/containers/InAppNotification/NotifierComponent.stories.tsx:46-75
Timestamp: 2026-03-10T15:21:45.098Z
Learning: In `app/containers/InAppNotification/NotifierComponent.tsx` (React Native, Rocket.Chat), `NotifierComponent` is exported as a Redux-connected component via `connect(mapStateToProps)`. The `isMasterDetail` prop is automatically injected from `state.app.isMasterDetail` and does not need to be passed explicitly at call sites or in Storybook stories that use the default (connected) export.

Applied to files:

  • app/containers/RoomHeader/index.tsx
  • app/views/RoomView/index.tsx
🔇 Additional comments (25)
android/app/src/main/res/values/ids.xml (1)

1-5: LGTM!

The resource ID declaration follows Android conventions and properly supports the native view tagging mechanism for inverted list detection.

app/containers/RoomHeader/RoomHeader.tsx (1)

138-174: LGTM!

The forwardRef conversion and useImperativeHandle implementation correctly expose an imperative focus() method. The use of findNodeHandle with AccessibilityInfo.setAccessibilityFocus is the appropriate pattern for programmatic accessibility focus in React Native.

app/containers/RoomHeader/index.tsx (1)

27-104: LGTM!

The React.memo(React.forwardRef(...)) composition is correctly implemented, properly forwarding the ref to the inner RoomHeader component while maintaining memoization benefits.

app/containers/MessageComposer/interfaces.ts (1)

5-10: LGTM!

The focus() method addition to IMessageComposerRef aligns with the existing IComposerInput.focus() contract and provides a consistent imperative API for programmatic focus control.

app/views/RoomsListView/index.tsx (1)

76-76: LGTM!

Passing focusHeaderOnOpen: true enables proper keyboard focus management when navigating to rooms. The goRoom helper correctly applies this only in master-detail mode.

app/views/RoomView/List/components/List.tsx (1)

47-49: LGTM!

The exitFocusNativeId prop correctly configures the Android-specific focus exit target, enabling keyboard navigation to reach the message composer from the inverted message list.

app/stacks/types.ts (1)

44-44: LGTM!

The optional focusHeaderOnOpen parameter correctly extends the RoomView route params type to support the keyboard focus navigation feature.

app/lib/methods/helpers/goRoom.ts (2)

28-47: LGTM!

The focusHeaderOnOpen parameter integration correctly extends the navigation helper to support header focus in master-detail mode while preserving existing route param behavior.


94-104: LGTM!

The type declaration properly documents the new optional focusHeaderOnOpen parameter alongside existing navigation options.

android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java (1)

27-30: LGTM!

The @ReactProp implementation follows React Native conventions correctly, forwarding the boolean prop to the native view.

android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java (1)

29-32: LGTM!

The @ReactProp for exitFocusNativeId is correctly implemented with @Nullable annotation for the optional string prop.

android/app/src/main/java/chat/rocket/reactnative/scroll/FocusUtils.java (1)

14-28: LGTM!

The utility method is well-implemented with proper null safety and type checking. The parent chain traversal is safe and correctly terminates when reaching a non-View parent.

android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java (2)

39-53: Consider checking focusability in addition to visibility.

In onRequestFocusInDescendants, the loop checks child.getVisibility() == VISIBLE but doesn't verify if the child is actually focusable before calling requestFocus(). While requestFocus() will return false for non-focusable views, you could optimize by also checking child.isFocusable() or use child.requestFocus() directly as you're doing (since it handles this internally).

The current implementation is functionally correct, just noting this as a minor observation.


55-77: LGTM!

The addFocusables override correctly reverses only the direct children's focusables in the list while preserving the order of focusables from nested descendants. The in-place swap algorithm is efficient.

app/views/RoomView/index.tsx (2)

258-267: LGTM!

The focus handling logic is well-structured:

  • Uses InteractionManager.runAfterInteractions to avoid animation conflicts
  • Properly clears focusHeaderOnOpen param after focusing to prevent repeated focus
  • Correctly falls back to composer focus when header focus isn't needed

1574-1578: LGTM!

The nativeID='message-composer-exit-focus' correctly matches the exitFocusNativeId prop passed to the inverted scroll component, enabling focus to exit the inverted list and land on the composer.

android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11ySpec.java (1)

9-30: LGTM!

The TurboModule spec follows React Native conventions correctly with proper @ReactMethod annotations and the async Promise parameter for getState.

app/views/RoomView/List/components/InvertedScrollView.tsx (1)

1-10: LGTM!

Clean re-export pattern with proper TypeScript typing. The exitFocusNativeId prop extension aligns with the native Android implementation.

app/lib/native/KeyboardInversionA11yAndroid.ts (1)

21-25: LGTM!

Clean TurboModule wrapper with proper type definitions.

android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java (1)

95-136: LGTM!

The helper methods findContainingCellIndex, findExitTarget, and isDescendantOf are well-implemented. The navigation logic correctly handles the inverted view-tree order by reversing the iteration direction.

app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx (3)

50-58: LGTM!

Proper keyboard listener lifecycle management with cleanup in componentWillUnmount.


172-203: LGTM!

The render method correctly composes styles, forwards props including exitFocusNativeId, and properly wraps children in the native content view with collapsable={false} to prevent view flattening.


224-228: LGTM!

Clean forwardRef wrapper with displayName for easier debugging.

app/containers/Button/Button.test.tsx (1)

67-67: Comment update is accurate and helpful.

The revised note correctly documents the Jest/RNGestureHandlerButton mock behavior after migrating to RectButton.

app/containers/Button/index.tsx (1)

3-3: RectButton prop-surface migration looks clean.

Good move omitting enabled from consumer props and mapping external disabled to internal enablement logic.

Also applies to: 9-9, 20-20

Comment on lines +30 to +40
@Override
public void enable(String scope) {
sEnabled = true;
sScope = scope;
}

@Override
public void disable() {
sEnabled = false;
sScope = null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Non-atomic compound write may produce inconsistent state under concurrent access.

The enable() method writes two volatile fields (sEnabled then sScope) without synchronization. If MainActivity.dispatchKeyEvent or getState() reads between these two writes, it could observe enabled=true with scope=null, or vice versa during disable().

For this use case (screen lifecycle toggles), the practical risk is low, but consider using a single atomic reference or synchronization if correctness matters:

🔒 Proposed fix using synchronized block
-  private static volatile boolean sEnabled = false;
-  `@Nullable`
-  private static volatile String sScope = null;
+  private static final Object sLock = new Object();
+  private static boolean sEnabled = false;
+  `@Nullable`
+  private static String sScope = null;

+  public static boolean isEnabled() {
+    synchronized (sLock) {
+      return sEnabled;
+    }
+  }

+  `@Nullable`
+  public static String getScope() {
+    synchronized (sLock) {
+      return sScope;
+    }
+  }

   `@Override`
   public void enable(String scope) {
+    synchronized (sLock) {
       sEnabled = true;
       sScope = scope;
+    }
   }

   `@Override`
   public void disable() {
+    synchronized (sLock) {
       sEnabled = false;
       sScope = null;
+    }
   }

   `@Override`
   public void getState(Promise promise) {
+    synchronized (sLock) {
       WritableMap state = com.facebook.react.bridge.Arguments.createMap();
       state.putBoolean("enabled", sEnabled);
       state.putString("scope", sScope);
       promise.resolve(state);
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java`
around lines 30 - 40, The enable()/disable() methods perform two separate
volatile writes to sEnabled and sScope which can be observed in an inconsistent
intermediate state by readers such as MainActivity.dispatchKeyEvent or getState;
wrap the compound write in a single atomic operation by synchronizing or using
an atomic holder: either place both assignments inside a synchronized block
(e.g., synchronized on KeyboardA11yModule.class) in enable(String) and
disable(), or replace sEnabled/sScope with a single AtomicReference or immutable
state object (e.g., KeyboardA11yState) updated atomically; update readers
(getState, dispatchKeyEvent) to read the single synchronized/atomic state to
ensure visibility and consistency.

Comment on lines +47 to +82
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (KeyboardA11yModule.isEnabled()) {
val current: View? = currentFocus
if (current != null && FocusUtils.hasInvertedParent(current)) {
if (event.action == KeyEvent.ACTION_DOWN) {
val keyCode = event.keyCode
val isShiftPressed = event.isShiftPressed
val mapped = when (keyCode) {
// Invert DPAD vertical arrows for inverted lists
KeyEvent.KEYCODE_DPAD_DOWN -> KeyEvent.KEYCODE_DPAD_UP
KeyEvent.KEYCODE_DPAD_UP -> KeyEvent.KEYCODE_DPAD_DOWN
// Map Tab / Shift+Tab to vertical navigation as well
KeyEvent.KEYCODE_TAB ->
if (isShiftPressed) KeyEvent.KEYCODE_DPAD_UP else KeyEvent.KEYCODE_DPAD_DOWN
else -> keyCode
}
if (mapped != keyCode) {
val invertedEvent = KeyEvent(
event.downTime,
event.eventTime,
event.action,
mapped,
event.repeatCount,
event.metaState,
event.deviceId,
event.scanCode,
event.flags,
event.source
)
return super.dispatchKeyEvent(invertedEvent)
}
}
}
}
return super.dispatchKeyEvent(event)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential issue: ACTION_UP events are not remapped.

The remapping logic only handles ACTION_DOWN events (Line 51). When a key is pressed, Android generates both ACTION_DOWN and ACTION_UP events. By only remapping ACTION_DOWN, the system receives mismatched key codes:

  • ACTION_DOWN with KEYCODE_DPAD_UP (remapped)
  • ACTION_UP with KEYCODE_DPAD_DOWN (original)

This mismatch could cause unpredictable behavior in some scenarios, such as keys appearing "stuck" or focus behavior not completing properly.

🔧 Proposed fix to also remap ACTION_UP
   override fun dispatchKeyEvent(event: KeyEvent): Boolean {
     if (KeyboardA11yModule.isEnabled()) {
       val current: View? = currentFocus
       if (current != null && FocusUtils.hasInvertedParent(current)) {
-        if (event.action == KeyEvent.ACTION_DOWN) {
+        if (event.action == KeyEvent.ACTION_DOWN || event.action == KeyEvent.ACTION_UP) {
           val keyCode = event.keyCode
           val isShiftPressed = event.isShiftPressed
           val mapped = when (keyCode) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt` around
lines 47 - 82, The current dispatchKeyEvent remaps only ACTION_DOWN events
causing mismatched ACTION_UP keycodes; update the condition that checks the
event action in dispatchKeyEvent (used with KeyboardA11yModule.isEnabled() and
FocusUtils.hasInvertedParent(current)) to also handle KeyEvent.ACTION_UP (e.g.,
if event.action == KeyEvent.ACTION_DOWN || event.action == KeyEvent.ACTION_UP),
perform the same keyCode mapping logic for ACTION_UP, and construct/dispatch the
inverted KeyEvent (preserving downTime, eventTime, repeatCount, metaState,
deviceId, scanCode, flags, source) so both press and release events are
consistently remapped.

return false;
}

ViewGroup contentView = (ViewGroup) getChildAt(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing type check before cast.

getChildAt(0) returns View, not necessarily ViewGroup. If the content view hierarchy changes, this will throw ClassCastException. Add a defensive check:

🛡️ Proposed defensive check
-    ViewGroup contentView = (ViewGroup) getChildAt(0);
+    View child = getChildAt(0);
+    if (!(child instanceof ViewGroup)) {
+      return false;
+    }
+    ViewGroup contentView = (ViewGroup) child;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ViewGroup contentView = (ViewGroup) getChildAt(0);
View child = getChildAt(0);
if (!(child instanceof ViewGroup)) {
return false;
}
ViewGroup contentView = (ViewGroup) child;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java`
at line 69, In InvertedScrollView.java the code casts getChildAt(0) directly to
ViewGroup into variable contentView which can throw ClassCastException if the
child is not a ViewGroup; change this to first retrieve the child into a View,
check "instanceof ViewGroup" (and for null), then assign to contentView only
when the check passes, and handle the else case safely (e.g., return early, fall
back to a no-op, or log an error) so the view hierarchy change won't crash the
method.

Comment on lines +85 to +92
int exitDir = isForward ? View.FOCUS_DOWN : View.FOCUS_UP;
View exitTarget = findExitTarget(exitDir);
if (exitTarget != null) {
exitTarget.requestFocus();
return true;
}

return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Key consumed at boundary without visual feedback.

When at the list boundary and no exit target is found, returning true consumes the key event without moving focus anywhere. This could trap keyboard users at the list edge without indication.

Consider either returning false to allow default handling, or providing feedback (e.g., scroll bounce, accessibility announcement):

💡 Alternative: allow default handling at boundary
     View exitTarget = findExitTarget(exitDir);
     if (exitTarget != null) {
       exitTarget.requestFocus();
       return true;
     }
 
-    return true;
+    return false; // Let system handle boundary case
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java`
around lines 85 - 92, The current focus-handling in InvertedScrollView (around
variables exitDir and exitTarget) always returns true even when
findExitTarget(exitDir) returns null, which consumes the key at the list
boundary and can trap keyboard users; update the method so that when exitTarget
== null it does not consume the event—either return false to allow default focus
handling or trigger accessible feedback (e.g., call
announceForAccessibility(...) or an EdgeEffect bounce) before returning true;
locate the block using exitDir/exitTarget in InvertedScrollView and replace the
final unconditional "return true" with logic that returns false when exitTarget
is null (or invokes an accessibility feedback helper then returns true) so users
receive proper feedback or focus moves.

Comment on lines +91 to 101
<RectButton
onPress={onPress}
disabled={isDisabled}
enabled={!isDisabled}
activeOpacity={0.2}
underlayColor='transparent'
// No Android ink ripple; matches Pressable with `android_ripple` disabled.
rippleColor='transparent'
style={containerStyle}
accessibilityLabel={title}
accessibilityRole='button'
{...otherProps}>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For react-native-gesture-handler v2.24.0, does RectButton automatically expose disabled accessibility semantics when enabled={false}on Android/iOS, or isaccessibilityState={{ disabled: true }} still required for TalkBack/VoiceOver announcement?

💡 Result:

No, RectButton does not automatically expose disabled accessibility semantics when enabled={false}. The accessibilityState={{ disabled: true }} prop is still required for proper TalkBack (Android) and VoiceOver (iOS) announcements of the disabled state.

Citations:


Add explicit accessibility state to indicate disabled buttons on RectButton.

When using RectButton with enabled={!isDisabled}, the disabled state is not automatically announced to screen readers (TalkBack on Android, VoiceOver on iOS). The accessibilityState={{ disabled: !!isDisabled }} prop must be set explicitly for proper accessibility announcements.

Suggested patch
 		<RectButton
 			onPress={onPress}
 			enabled={!isDisabled}
+			accessibilityState={{ disabled: !!isDisabled }}
 			activeOpacity={0.2}
 			underlayColor='transparent'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<RectButton
onPress={onPress}
disabled={isDisabled}
enabled={!isDisabled}
activeOpacity={0.2}
underlayColor='transparent'
// No Android ink ripple; matches Pressable with `android_ripple` disabled.
rippleColor='transparent'
style={containerStyle}
accessibilityLabel={title}
accessibilityRole='button'
{...otherProps}>
<RectButton
onPress={onPress}
enabled={!isDisabled}
accessibilityState={{ disabled: !!isDisabled }}
activeOpacity={0.2}
underlayColor='transparent'
// No Android ink ripple; matches Pressable with `android_ripple` disabled.
rippleColor='transparent'
style={containerStyle}
accessibilityLabel={title}
accessibilityRole='button'
{...otherProps}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Button/index.tsx` around lines 91 - 101, Add an explicit
accessibilityState to the RectButton so screen readers announce when the button
is disabled: update the RectButton (in this file's Button component) to include
accessibilityState={{ disabled: !!isDisabled }} alongside existing props (e.g.,
onPress, enabled={!isDisabled}, accessibilityLabel={title}) so the disabled
state is conveyed to assistive tech.

Copy link
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/containers/MessageComposer/interfaces.ts (1)

5-10: ⚠️ Potential issue | 🔴 Critical

Add focus() to MessageComposer's useImperativeHandle.

The interface now requires focus() on IMessageComposerRef, but MessageComposer.tsx lines 55-59 do not expose it in useImperativeHandle. The currently exposed methods are only closeEmojiKeyboardAndAction, getText, and setInput. This will cause runtime failures when external code calls composerRef.current.focus().

Current implementation
useImperativeHandle(forwardedRef, () => ({
	closeEmojiKeyboardAndAction,
	getText: composerInputComponentRef.current?.getText,
	setInput: composerInputComponentRef.current?.setInput
}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/MessageComposer/interfaces.ts` around lines 5 - 10, The
IMessageComposerRef now includes focus(), but MessageComposer.tsx's
useImperativeHandle currently only exposes closeEmojiKeyboardAndAction, getText
and setInput; update the useImperativeHandle(forwardedRef, ...) return object to
also expose focus by wiring it to composerInputComponentRef.current?.focus (or
the component's focus method), ensuring the forwardedRef provides a focus()
implementation that delegates to the inner composer input's focus method and
matches the IMessageComposerRef signature.
app/views/RoomView/index.tsx (1)

222-266: ⚠️ Potential issue | 🟠 Major

Read focusHeaderOnOpen from live props, not the mount-time route.

route is captured once in componentDidMount, so this listener keeps reading the original params. After Line 262 clears focusHeaderOnOpen, the callback can still see the old true and refocus the header every time the room regains focus.

Suggested fix
-		const { navigation, dispatch, isMasterDetail, route } = this.props;
+		const { navigation, dispatch, isMasterDetail } = this.props;
...
 		this.unsubscribeFocus = navigation.addListener('focus', () => {
 			InteractionManager.runAfterInteractions(() => {
-				if (isMasterDetail && route?.params?.focusHeaderOnOpen) {
+				if (isMasterDetail && this.props.route?.params?.focusHeaderOnOpen) {
 					this.roomHeaderRef.current?.focus();
 					navigation.setParams({ focusHeaderOnOpen: undefined });
 					return;
 				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/RoomView/index.tsx` around lines 222 - 266, The focus listener
currently closes over the mount-time route/navigation variables so it keeps
reading the original route.params.focusHeaderOnOpen; update the
navigation.addListener('focus', ...) callback (the code that references
isMasterDetail, route?.params?.focusHeaderOnOpen, roomHeaderRef,
messageComposerRef, and navigation.setParams) to read the live props instead
(e.g. const focusHeaderOnOpen = this.props.route?.params?.focusHeaderOnOpen and
use this.props.navigation.setParams) or use this.props.navigation.getParam, and
similarly use this.props.navigation and this.props.route inside the callback so
clearing focusHeaderOnOpen actually takes effect on subsequent focuses.
♻️ Duplicate comments (3)
android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt (1)

51-76: ⚠️ Potential issue | 🟡 Minor

Remap the matching ACTION_UP event too.

Android sends paired down/up events. Right now the press is remapped but the release keeps the original keycode, so the framework sees mixed pairs for the same navigation step.

Suggested fix
-        if (event.action == KeyEvent.ACTION_DOWN) {
+        if (event.action == KeyEvent.ACTION_DOWN || event.action == KeyEvent.ACTION_UP) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt` around
lines 51 - 76, The current dispatchKeyEvent remaps only ACTION_DOWN events (uses
keyCode -> mapped and builds invertedEvent) but leaves ACTION_UP unmapped
causing mismatched pairs; update dispatchKeyEvent to also detect
KeyEvent.ACTION_UP, compute the same mapped key (reuse the mapped logic for
keyCode), construct an invertedEvent for ACTION_UP preserving event.downTime,
event.eventTime, repeatCount, metaState, deviceId, scanCode, flags, source, and
call return super.dispatchKeyEvent(invertedEvent) for the UP case as well so
both press and release use the same mapped key; keep existing behavior for other
actions.
android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java (2)

69-69: ⚠️ Potential issue | 🟡 Minor

Missing type check before cast remains unaddressed.

The direct cast (ViewGroup) getChildAt(0) can throw ClassCastException if the first child isn't a ViewGroup. Add a defensive check as previously suggested.

🛡️ Proposed defensive check
-    ViewGroup contentView = (ViewGroup) getChildAt(0);
+    View child = getChildAt(0);
+    if (!(child instanceof ViewGroup)) {
+      return false;
+    }
+    ViewGroup contentView = (ViewGroup) child;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java`
at line 69, In InvertedScrollView, avoid the unsafe cast to ViewGroup by
checking the child first: call getChildAt(0) into a local (e.g., child), verify
child != null and child instanceof ViewGroup before casting to contentView, and
handle the else case (e.g., skip processing, return, or log a warning) so a
ClassCastException cannot occur when the first child is not a ViewGroup.

85-92: ⚠️ Potential issue | 🟡 Minor

Boundary key consumption issue remains unaddressed.

When at the list boundary with no exit target found, returning true (line 92) consumes the key event without moving focus anywhere. This traps keyboard users. Consider returning false to allow default system handling.

💡 Proposed fix
     View exitTarget = findExitTarget(exitDir);
     if (exitTarget != null) {
       exitTarget.requestFocus();
       return true;
     }
 
-    return true;
+    return false; // Let system handle boundary case
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java`
around lines 85 - 92, The key handler in InvertedScrollView currently returns
true even when findExitTarget(exitDir) returns null, which consumes boundary key
events and traps keyboard users; update the logic in the method handling
directional focus (using exitDir and exitTarget) so that if exitTarget is null
you return false (allowing default system handling) instead of true, while
keeping the existing behavior of requesting focus and returning true when
exitTarget != null.
🧹 Nitpick comments (2)
app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx (1)

36-48: Heavy reliance on any types reduces type safety.

Multiple refs and handlers use any, which bypasses TypeScript's type checking. While this is common with native components, consider using more specific types where possible:

  • scrollRef could be typed as React.RefObject<typeof NativeInvertedScrollView> if the native component exposes typings
  • State interface is well-defined, which is good

This is acceptable for now given native component limitations, but worth revisiting if the native layer adds TypeScript definitions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx` around lines
36 - 48, The class RNLikeInvertedScrollView uses broad any types for scrollRef
and keyboard subscription fields; replace them with more specific types (e.g.,
change scrollRef: React.createRef<any>() to
React.createRef<NativeInvertedScrollView | null>() or React.RefObject<ScrollView
| null> depending on the native component type you can import), and type the
keyboard subscription fields as { remove(): void } | undefined instead of any;
update the declarations for _subscriptionKeyboardDidShow,
_subscriptionKeyboardDidHide, and _keyboardMetrics (e.g., { height: number } |
null already ok) and adjust any handler params/signatures that currently use any
to use the appropriate event/interface from React Native so TypeScript can check
usages in RNLikeInvertedScrollView.
app/lib/native/KeyboardInversionA11yAndroid.ts (1)

15-15: Consider adding a platform check for future safety, though currently unused.

This module has no call sites in the codebase currently. However, since TurboModuleRegistry.getEnforcing will throw on iOS, consider adding a platform check at module load time to prevent crashes if this is imported from iOS code in the future:

+import { Platform } from 'react-native';
+
+if (Platform.OS !== 'android') {
+  throw new Error('KeyboardInversionA11yAndroid is only available on Android');
+}
+
 const NativeModule = TurboModuleRegistry.getEnforcing<Spec>('KeyboardA11y');

Alternatively, document the Android-only requirement in JSDoc.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/native/KeyboardInversionA11yAndroid.ts` at line 15, The file
currently calls TurboModuleRegistry.getEnforcing<Spec>('KeyboardA11y') at module
load (const NativeModule =
TurboModuleRegistry.getEnforcing<Spec>('KeyboardA11y')) which will throw on
non-Android platforms; wrap this call in a Platform.OS === 'android' guard so
the TurboModule is only resolved on Android and export a safe null/undefined
fallback (or no-op shim) for other platforms, and add a short JSDoc note on the
KeyboardInversionA11yAndroid export indicating it is Android-only;
alternatively, if you prefer not to add runtime branching, add the Android-only
JSDoc comment above the module to document the requirement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java`:
- Around line 40-52: The onRequestFocusInDescendants method currently always
iterates children from last-to-first; when mIsInvertedContent is true you must
respect the direction parameter: if direction == View.FOCUS_UP iterate forward
from index 0 to getChildCount()-1 so the bottom-most child (index 0 in inverted
content) is tried first, otherwise iterate backward from getChildCount()-1 to 0;
update the loop in onRequestFocusInDescendants to choose the iteration order
based on the direction before calling getChildAt(...) and
child.requestFocus(...).

---

Outside diff comments:
In `@app/containers/MessageComposer/interfaces.ts`:
- Around line 5-10: The IMessageComposerRef now includes focus(), but
MessageComposer.tsx's useImperativeHandle currently only exposes
closeEmojiKeyboardAndAction, getText and setInput; update the
useImperativeHandle(forwardedRef, ...) return object to also expose focus by
wiring it to composerInputComponentRef.current?.focus (or the component's focus
method), ensuring the forwardedRef provides a focus() implementation that
delegates to the inner composer input's focus method and matches the
IMessageComposerRef signature.

In `@app/views/RoomView/index.tsx`:
- Around line 222-266: The focus listener currently closes over the mount-time
route/navigation variables so it keeps reading the original
route.params.focusHeaderOnOpen; update the navigation.addListener('focus', ...)
callback (the code that references isMasterDetail,
route?.params?.focusHeaderOnOpen, roomHeaderRef, messageComposerRef, and
navigation.setParams) to read the live props instead (e.g. const
focusHeaderOnOpen = this.props.route?.params?.focusHeaderOnOpen and use
this.props.navigation.setParams) or use this.props.navigation.getParam, and
similarly use this.props.navigation and this.props.route inside the callback so
clearing focusHeaderOnOpen actually takes effect on subsequent focuses.

---

Duplicate comments:
In `@android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt`:
- Around line 51-76: The current dispatchKeyEvent remaps only ACTION_DOWN events
(uses keyCode -> mapped and builds invertedEvent) but leaves ACTION_UP unmapped
causing mismatched pairs; update dispatchKeyEvent to also detect
KeyEvent.ACTION_UP, compute the same mapped key (reuse the mapped logic for
keyCode), construct an invertedEvent for ACTION_UP preserving event.downTime,
event.eventTime, repeatCount, metaState, deviceId, scanCode, flags, source, and
call return super.dispatchKeyEvent(invertedEvent) for the UP case as well so
both press and release use the same mapped key; keep existing behavior for other
actions.

In
`@android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java`:
- Line 69: In InvertedScrollView, avoid the unsafe cast to ViewGroup by checking
the child first: call getChildAt(0) into a local (e.g., child), verify child !=
null and child instanceof ViewGroup before casting to contentView, and handle
the else case (e.g., skip processing, return, or log a warning) so a
ClassCastException cannot occur when the first child is not a ViewGroup.
- Around line 85-92: The key handler in InvertedScrollView currently returns
true even when findExitTarget(exitDir) returns null, which consumes boundary key
events and traps keyboard users; update the logic in the method handling
directional focus (using exitDir and exitTarget) so that if exitTarget is null
you return false (allowing default system handling) instead of true, while
keeping the existing behavior of requesting focus and returning true when
exitTarget != null.

---

Nitpick comments:
In `@app/lib/native/KeyboardInversionA11yAndroid.ts`:
- Line 15: The file currently calls
TurboModuleRegistry.getEnforcing<Spec>('KeyboardA11y') at module load (const
NativeModule = TurboModuleRegistry.getEnforcing<Spec>('KeyboardA11y')) which
will throw on non-Android platforms; wrap this call in a Platform.OS ===
'android' guard so the TurboModule is only resolved on Android and export a safe
null/undefined fallback (or no-op shim) for other platforms, and add a short
JSDoc note on the KeyboardInversionA11yAndroid export indicating it is
Android-only; alternatively, if you prefer not to add runtime branching, add the
Android-only JSDoc comment above the module to document the requirement.

In `@app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx`:
- Around line 36-48: The class RNLikeInvertedScrollView uses broad any types for
scrollRef and keyboard subscription fields; replace them with more specific
types (e.g., change scrollRef: React.createRef<any>() to
React.createRef<NativeInvertedScrollView | null>() or React.RefObject<ScrollView
| null> depending on the native component type you can import), and type the
keyboard subscription fields as { remove(): void } | undefined instead of any;
update the declarations for _subscriptionKeyboardDidShow,
_subscriptionKeyboardDidHide, and _keyboardMetrics (e.g., { height: number } |
null already ok) and adjust any handler params/signatures that currently use any
to use the appropriate event/interface from React Native so TypeScript can check
usages in RNLikeInvertedScrollView.
🪄 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: 1c5841f3-c217-42c5-8fd1-fcfe793941e1

📥 Commits

Reviewing files that changed from the base of the PR and between b71ee89 and 4098eed.

⛔ Files ignored due to path filters (6)
  • app/containers/Button/__snapshots__/Button.test.tsx.snap is excluded by !**/*.snap
  • app/containers/LoginServices/__snapshots__/LoginServices.test.tsx.snap is excluded by !**/*.snap
  • app/containers/UIKit/__snapshots__/UiKitMessage.test.tsx.snap is excluded by !**/*.snap
  • app/containers/UIKit/__snapshots__/UiKitModal.test.tsx.snap is excluded by !**/*.snap
  • app/containers/message/__snapshots__/Message.test.tsx.snap is excluded by !**/*.snap
  • app/views/CannedResponsesListView/__snapshots__/CannedResponseItem.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (22)
  • android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
  • android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java
  • android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11ySpec.java
  • android/app/src/main/java/chat/rocket/reactnative/scroll/FocusUtils.java
  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java
  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java
  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java
  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java
  • android/app/src/main/res/values/ids.xml
  • app/containers/Button/Button.test.tsx
  • app/containers/Button/index.tsx
  • app/containers/MessageComposer/interfaces.ts
  • app/containers/RoomHeader/RoomHeader.tsx
  • app/containers/RoomHeader/index.tsx
  • app/lib/methods/helpers/goRoom.ts
  • app/lib/native/KeyboardInversionA11yAndroid.ts
  • app/stacks/types.ts
  • app/views/RoomView/List/components/InvertedScrollView.tsx
  • app/views/RoomView/List/components/List.tsx
  • app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx
  • app/views/RoomView/index.tsx
  • app/views/RoomsListView/index.tsx
📜 Review details
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-12-17T15:56:22.578Z
Learnt from: OtavioStasiak
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6499
File: app/containers/ServerItem/index.tsx:34-36
Timestamp: 2025-12-17T15:56:22.578Z
Learning: In the Rocket.Chat React Native codebase, for radio button components on iOS, include the selection state ("Selected"/"Unselected") in the accessibilityLabel instead of using accessibilityState={{ checked: hasCheck }}, because iOS VoiceOver has known issues with accessibilityRole="radio" + accessibilityState that prevent correct state announcement.

Applied to files:

  • android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java
  • app/containers/Button/index.tsx
  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java
  • app/containers/RoomHeader/RoomHeader.tsx
📚 Learning: 2026-03-10T15:21:45.098Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7046
File: app/containers/InAppNotification/NotifierComponent.stories.tsx:46-75
Timestamp: 2026-03-10T15:21:45.098Z
Learning: In `app/containers/InAppNotification/NotifierComponent.tsx` (React Native, Rocket.Chat), `NotifierComponent` is exported as a Redux-connected component via `connect(mapStateToProps)`. The `isMasterDetail` prop is automatically injected from `state.app.isMasterDetail` and does not need to be passed explicitly at call sites or in Storybook stories that use the default (connected) export.

Applied to files:

  • app/containers/RoomHeader/index.tsx
  • app/views/RoomView/index.tsx
📚 Learning: 2026-03-04T20:13:17.288Z
Learnt from: divyanshu-patil
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6957
File: ios/RocketChat Watch App/Views/MessageComposerView.swift:37-55
Timestamp: 2026-03-04T20:13:17.288Z
Learning: In the WatchOS app (ios/RocketChat Watch App) for Rocket.Chat React Native, using SwiftUI `Button` inside a `ScrollView` on WatchOS causes accidental message sends because button tap targets can be triggered during scroll gestures. `Text` with `.onTapGesture` is the preferred pattern for tappable items in scroll views on WatchOS. To preserve accessibility, add `.accessibilityAddTraits(.isButton)` and `.accessibilityLabel()` to the `Text` element instead.

Applied to files:

  • app/containers/Button/index.tsx
  • android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java
  • app/containers/RoomHeader/RoomHeader.tsx
📚 Learning: 2026-03-17T19:15:30.463Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6970
File: .maestro/tests/room/share-message.yaml:77-79
Timestamp: 2026-03-17T19:15:30.463Z
Learning: In `.maestro/tests/room/share-message.yaml` (Rocket.Chat React Native), the `tapOn: point: 5%,10%` step is intentional: it taps the empty area above the bottom sheet and keyboard to dismiss both simultaneously. Using `action-sheet-handle` instead would only close the sheet but not the keyboard. This pattern is acceptable when both need to be dismissed together in a single step.

Applied to files:

  • android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
🔇 Additional comments (17)
app/containers/Button/index.tsx (2)

91-101: Missing accessibilityState for disabled button announcement.

The RectButton does not automatically expose disabled accessibility semantics when enabled={false}. Screen readers (TalkBack on Android, VoiceOver on iOS) require explicit accessibilityState={{ disabled: !!isDisabled }} to announce the disabled state.

Suggested fix
 		<RectButton
 			onPress={onPress}
 			enabled={!isDisabled}
+			accessibilityState={{ disabled: !!isDisabled }}
 			activeOpacity={0.2}
 			underlayColor='transparent'

3-3: LGTM on the interface changes.

The interface properly extends RectButtonProps while omitting children and enabled to avoid conflicts. Adding the explicit disabled?: boolean prop maintains a clean API for consumers.

Also applies to: 9-21

app/containers/Button/Button.test.tsx (1)

64-69: LGTM!

The updated comment accurately documents the testing limitation with RNGestureHandlerButton in Jest. The assertion correctly validates the enabled prop for the new RectButton implementation.

android/app/src/main/res/values/ids.xml (1)

1-5: LGTM!

The resource ID declaration is correctly formatted and follows Android naming conventions. This ID will serve as a tag marker for inverted list detection in the focus navigation logic.

app/containers/RoomHeader/RoomHeader.tsx (1)

86-88: LGTM!

The imperative focus API is correctly implemented using the standard React Native pattern with findNodeHandle and AccessibilityInfo.setAccessibilityFocus. The null check on nodeHandle properly guards against the case where the view hasn't mounted yet.

Also applies to: 138-174

app/containers/RoomHeader/index.tsx (1)

27-107: LGTM!

The ref forwarding is correctly implemented with React.memo(React.forwardRef(...)) pattern. The IRoomHeaderRef type re-export enables proper typing at consumer sites.

app/views/RoomView/List/components/List.tsx (1)

47-49: LGTM!

The exitFocusNativeId prop is correctly conditionally passed only for non-iOS platforms, aligning with the Android-specific keyboard navigation fix. The hardcoded string 'message-composer-exit-focus' should match the nativeID set on the message composer wrapper in RoomView.

app/views/RoomsListView/index.tsx (1)

74-77: LGTM!

Adding focusHeaderOnOpen: true ensures that when users navigate to a room from the rooms list (especially via keyboard), the accessibility focus moves to the room header. This is the correct behavior for keyboard navigation flow.

app/stacks/types.ts (1)

44-44: LGTM!

The optional focusHeaderOnOpen parameter is correctly typed and positioned within the RoomView route params.

android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java (1)

28-32: LGTM!

The @ReactProp registration follows standard React Native view manager patterns. The nullable annotation correctly handles cases where the prop is not set from JavaScript.

app/views/RoomView/List/components/InvertedScrollView.tsx (1)

1-10: Clean re-export pattern for the new inverted scroll implementation.

The type cast approach correctly exposes InvertedScrollViewProps while hiding the internal scrollViewRef prop that's managed by the forwardRef wrapper. The exitFocusNativeId prop is properly typed and flows through to the native layer.

app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx (3)

50-58: LGTM!

Keyboard listener lifecycle is properly managed with setup in componentDidMount and teardown in componentWillUnmount using optional chaining for safety.


68-75: Animation detection logic is well-designed.

The isAnimating check using momentum scroll timing with a 16ms threshold (one frame at 60fps) effectively detects ongoing scroll animations. This prevents touch events from being stolen during smooth scrolling.


172-203: Render composition is correct.

Destructuring onContentSizeChange from rest prevents passing it to the native scroll view (which doesn't support it), while handleContentOnLayout on the content view correctly emulates this behavior. The style composition and prop forwarding are well-structured.

android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java (3)

95-106: LGTM!

findContainingCellIndex correctly traverses the view hierarchy to locate the containing cell, with proper handling for the edge case where a parent isn't a View.


108-136: Well-structured exit target resolution.

The two-phase approach (nativeID lookup then FocusFinder fallback) provides flexibility while maintaining system-like focus behavior. The isDescendantOf check correctly prevents focus from cycling back into the scroll view.


35-57: Key event dispatch is correctly structured.

The mKeyConsumed flag pattern correctly handles the ACTION_DOWN/ACTION_UP pairing, processing navigation on ACTION_DOWN and returning the cached result for ACTION_UP. Direction mapping is correct for both DPAD and TAB keys.

Comment on lines +40 to +52
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
if (mIsInvertedContent) {
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (child.getVisibility() == VISIBLE) {
if (child.requestFocus(direction, previouslyFocusedRect)) {
return true;
}
}
}
return false;
}
return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and read the InvertedScrollContentView file
find . -name "InvertedScrollContentView.java" -type f

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 167


🏁 Script executed:

# Check the app/views/RoomView/index.tsx file around lines 1574-1577
find . -path "*/app/views/RoomView/index.tsx" -type f

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 108


🏁 Script executed:

# Search for the onRequestFocusInDescendants implementation
rg "onRequestFocusInDescendants" -A 15 --type java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3573


🏁 Script executed:

# Read the RoomView/index.tsx file around lines 1574-1577
sed -n '1570,1580p' ./app/views/RoomView/index.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 238


🏁 Script executed:

# Get more context around the footer/composer addition
sed -n '1550,1600p' ./app/views/RoomView/index.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1426


🏁 Script executed:

# Read the full InvertedScrollContentView.java file to understand its structure
wc -l ./android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 170


🏁 Script executed:

# Read the complete InvertedScrollContentView.java file
cat -n ./android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3070


🏁 Script executed:

# Research Android View focus direction constants
rg "FOCUS_UP|FOCUS_DOWN|FOCUS_FORWARD|FOCUS_BACKWARD" android/app/src/main/java --type java -i

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 366


🏁 Script executed:

# Check if there are other focus-related implementations in the codebase that handle direction
rg "onRequestFocusInDescendants|addFocusables" android/app/src/main/java --type java -B 2 -A 10

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3762


🏁 Script executed:

# Search for any documentation or comments about focus direction handling
rg "FOCUS_UP|FOCUS_DOWN|direction" android/app/src/main/java/chat/rocket/reactnative/scroll/ --type java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1466


🏁 Script executed:

# Read the InvertedScrollView.java file to understand how it uses focus direction
cat -n ./android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java | head -100

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 4045


🏁 Script executed:

# Get more context from InvertedScrollView around focus direction handling
rg "findExitTarget|focusDir|exitDir" android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java -B 3 -A 8

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1058


🏁 Script executed:

# Final verification: check how direction constants are used in Android standard behavior
rg "FOCUS_UP|FOCUS_DOWN" android/app/src/main/java/chat/rocket/reactnative/scroll/ --type java -B 1 -A 1

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 777


Respect the direction parameter to determine iteration order.

When entering from the footer/composer below the list (with FOCUS_UP), the iteration should start from the bottom-most child (index 0), not the top. The current code always iterates last-to-first regardless of direction, causing focus to land on the wrong end of the list. Use the direction parameter to choose whether to iterate forward or backward through children.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java`
around lines 40 - 52, The onRequestFocusInDescendants method currently always
iterates children from last-to-first; when mIsInvertedContent is true you must
respect the direction parameter: if direction == View.FOCUS_UP iterate forward
from index 0 to getChildCount()-1 so the bottom-most child (index 0 in inverted
content) is tried first, otherwise iterate backward from getChildCount()-1 to 0;
update the loop in onRequestFocusInDescendants to choose the iteration order
based on the direction before calling getChildAt(...) and
child.requestFocus(...).

@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.71.0.108432

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNTtIwYdUnrVonpp1RfgQTAEX2ij3rh0R23ZYQhW3GqQEPZOmgSedjT5eSWj_xUeEZiNZq1RyP3wX_5QZ26W

@github-actions
Copy link

iOS Build Available

Rocket.Chat Experimental 4.71.0.108433

Copy link
Contributor

@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.

🧹 Nitpick comments (3)
app/containers/Touch.tsx (3)

50-51: Outdated comment references RectButton.

The comment still mentions "RectButton" but the component now uses Pressable. Update for accuracy.

📝 Suggested fix
-		// The background color must be applied to the RectButton, not the View.
-		// If set on the View, the touch opacity animation won't work properly.
+		// The background color must be applied to the Pressable, not the View.
+		// If set on the View, the touch opacity animation won't work properly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Touch.tsx` around lines 50 - 51, Update the outdated comment
in Touch (app/containers/Touch.tsx) to refer to Pressable instead of RectButton:
change the sentence that currently reads "The background color must be applied
to the RectButton, not the View." to "The background color must be applied to
the Pressable, not the View." so the comment accurately reflects the current
component usage in the Touch component.

67-69: Outdated comment references RectButton.

Same issue—update comment to reflect the switch to Pressable.

📝 Suggested fix
 		// The margin should be applied to the parent component.
-		// If set on the View, it will create an internal margin inside the RectButton.
+		// If set on the View, it will create an internal margin inside the Pressable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Touch.tsx` around lines 67 - 69, The inline comment above the
marginStyles constant still mentions RectButton even though the component now
uses Pressable; update that comment in Touch.tsx to reference Pressable (or
generically "Pressable/Touchable element") and clearly state that the margin
must be applied to the parent to avoid internal spacing inside the Pressable;
locate the comment immediately above const marginStyles and replace "RectButton"
with "Pressable" and adjust phrasing to match the current implementation.

23-23: Consider renaming rectButtonStyle prop.

This prop name references the now-removed RectButton. Since this is a public interface and other components may depend on it, you could deprecate it with an alias or rename it to something generic like containerStyle in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Touch.tsx` at line 23, The public prop rectButtonStyle on the
Touch component still references the removed RectButton; add a new generic prop
(e.g., containerStyle) to the TouchProps interface and update the Touch
component to accept both containerStyle and rectButtonStyle, mapping them to the
actual underlying style prop (merge containerStyle and rectButtonStyle, with
containerStyle taking precedence) so existing users don’t break; keep
rectButtonStyle as a deprecated alias and emit a single console.warn when
rectButtonStyle is passed (or both are passed) to guide users to containerStyle,
and update any prop-type/TS definitions and the component’s props
destructuring/usage to use the merged style for the rendered wrapper element.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/containers/Touch.tsx`:
- Around line 50-51: Update the outdated comment in Touch
(app/containers/Touch.tsx) to refer to Pressable instead of RectButton: change
the sentence that currently reads "The background color must be applied to the
RectButton, not the View." to "The background color must be applied to the
Pressable, not the View." so the comment accurately reflects the current
component usage in the Touch component.
- Around line 67-69: The inline comment above the marginStyles constant still
mentions RectButton even though the component now uses Pressable; update that
comment in Touch.tsx to reference Pressable (or generically "Pressable/Touchable
element") and clearly state that the margin must be applied to the parent to
avoid internal spacing inside the Pressable; locate the comment immediately
above const marginStyles and replace "RectButton" with "Pressable" and adjust
phrasing to match the current implementation.
- Line 23: The public prop rectButtonStyle on the Touch component still
references the removed RectButton; add a new generic prop (e.g., containerStyle)
to the TouchProps interface and update the Touch component to accept both
containerStyle and rectButtonStyle, mapping them to the actual underlying style
prop (merge containerStyle and rectButtonStyle, with containerStyle taking
precedence) so existing users don’t break; keep rectButtonStyle as a deprecated
alias and emit a single console.warn when rectButtonStyle is passed (or both are
passed) to guide users to containerStyle, and update any prop-type/TS
definitions and the component’s props destructuring/usage to use the merged
style for the rendered wrapper element.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3eb629a8-33d6-45b3-8abb-6a365995821d

📥 Commits

Reviewing files that changed from the base of the PR and between 4098eed and 9cf9ad0.

⛔ Files ignored due to path filters (2)
  • ios/Podfile.lock is excluded by !**/*.lock
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (6)
  • app/containers/Touch.tsx
  • app/containers/message/Reactions.tsx
  • app/containers/message/Thread.tsx
  • app/containers/message/Touchable.tsx
  • app/containers/message/index.tsx
  • package.json
✅ Files skipped from review due to trivial changes (2)
  • package.json
  • app/containers/message/Reactions.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-03-10T15:21:45.098Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7046
File: app/containers/InAppNotification/NotifierComponent.stories.tsx:46-75
Timestamp: 2026-03-10T15:21:45.098Z
Learning: In `app/containers/InAppNotification/NotifierComponent.tsx` (React Native, Rocket.Chat), `NotifierComponent` is exported as a Redux-connected component via `connect(mapStateToProps)`. The `isMasterDetail` prop is automatically injected from `state.app.isMasterDetail` and does not need to be passed explicitly at call sites or in Storybook stories that use the default (connected) export.

Applied to files:

  • app/containers/message/index.tsx
  • app/containers/message/Thread.tsx
  • app/containers/Touch.tsx
📚 Learning: 2026-03-04T20:13:17.288Z
Learnt from: divyanshu-patil
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6957
File: ios/RocketChat Watch App/Views/MessageComposerView.swift:37-55
Timestamp: 2026-03-04T20:13:17.288Z
Learning: In the WatchOS app (ios/RocketChat Watch App) for Rocket.Chat React Native, using SwiftUI `Button` inside a `ScrollView` on WatchOS causes accidental message sends because button tap targets can be triggered during scroll gestures. `Text` with `.onTapGesture` is the preferred pattern for tappable items in scroll views on WatchOS. To preserve accessibility, add `.accessibilityAddTraits(.isButton)` and `.accessibilityLabel()` to the `Text` element instead.

Applied to files:

  • app/containers/message/Touchable.tsx
  • app/containers/message/Thread.tsx
  • app/containers/Touch.tsx
🔇 Additional comments (6)
app/containers/message/Touchable.tsx (2)

24-25: Verify intentional fallback of onHoverOut to onLongPress.

When no onHoverOut handler is provided, this falls back to triggering onLongPress on hover-out. This means hovering away from the element will invoke the long-press action (typically showing message actions), which may cause unexpected behavior on desktop/mouse users.

If this is intentional for accessibility or specific UX requirements, please add a comment explaining the rationale. Otherwise, consider using a neutral fallback like undefined.


12-13: LGTM on keyboard focus integration.

The use of withKeyboardFocus HOC to wrap Pressable and the addition of the focusable prop correctly enables keyboard navigation for message items, aligning with the PR's objective to fix inverted keyboard navigation on Android.

Also applies to: 21-28

app/containers/message/index.tsx (1)

444-444: LGTM on exposing onThreadPress via context.

This correctly adds onThreadPress to the MessageContext.Provider value, enabling Thread.tsx (and potentially other child components) to invoke thread navigation via keyboard or press interaction. The wiring aligns with the existing onThreadPress method defined at lines 236-241.

app/containers/message/Thread.tsx (1)

10-10: LGTM on Thread keyboard accessibility.

The component correctly:

  • Imports and uses the keyboard-focusable Touchable wrapper
  • Consumes onThreadPress from MessageContext
  • Applies proper accessibility attributes (accessibilityRole='button', accessibilityLabel)
  • Preserves the testID for testing

This enables keyboard-driven thread navigation as intended by the PR.

Also applies to: 17-17, 28-35

app/containers/Touch.tsx (2)

80-106: LGTM on the Pressable implementation.

The migration from RectButton to Pressable is well-structured:

  • android_ripple provides native feedback on Android
  • Dynamic style callback handles pressed-state visuals
  • Accessibility props correctly applied to inner View
  • disabled prop correctly wired to Pressable

This aligns with the PR's goal of enabling proper keyboard navigation focus handling.


85-93: Dark theme press feedback is nearly imperceptible on iOS.

With activeOpacity defaulting to 1, iOS press feedback relies solely on backgroundColor shift from surfaceRoom to surfaceNeutral. In light mode, this shift is adequate (#FFFFFF#E4E7EA). However, in dark mode, the contrast is too subtle (#1F2329#2D3039). Components without explicit backgroundColor—such as message/User (titleContainer), RoomView/LoadMore (button), and ScreenLockedView (close)—show almost no visual feedback on press in dark theme.

Consider setting activeOpacity to a lower value (e.g., 0.8 or 0.7) as the default or for dark-theme variants to provide more obvious press feedback across iOS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant