Skip to content

feat(a11y): Physical Keyboard Navigation#7010

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

feat(a11y): Physical Keyboard Navigation#7010
OtavioStasiak wants to merge 52 commits intodevelopfrom
fix.android-inverted-keyboard-navigation

Conversation

@OtavioStasiak
Copy link
Copy Markdown
Contributor

@OtavioStasiak OtavioStasiak commented Feb 26, 2026

Proposed changes

Users should be able to navigate through the app using an external keyboard.

Issue(s)

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

How to test or reproduce

  • Login navigation;
  • Basic navigation in RoomsListView (open the drawer, navigate to DirectoryView);
  • In RoomsListView, open the ServersList and navigate through the content;
  • Navigate to RoomView (the list must not be inverted on Android or iOS);
  • Navigate through reactions and focus on the thread button;
  • Navigate to A11y and Appearance;
  • In A11y and Appearance, test switches and pickers.

What we should ensure:
1 - The RoomView message list must work as expected on both platforms;
2 - In master-detail, we must be able to focus on items inside the modal (Android issue);
3 - ActionSheet navigation must work as expected on both platforms.

What is not covered here:
1 - Keyboard navigation for room swipe actions.

PS: Must be tested on android, iOS, iPad and Android tablet.

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

    • Native keyboard accessibility toggle and improved keyboard navigation for inverted message lists, plus explicit exit-focus and header/composer focus control.
  • Accessibility

    • Exposed programmatic focus for room header and composer; added roles/labels and selected state for reactions and thread controls; improved switch row accessibility and toggles.
  • Bug Fixes

    • Consistent disabled button behavior and refined touch/press visuals.
  • Refactor

    • Reworked inverted list scrolling and responder/keyboard handling for more reliable keyboard/focus interactions.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Android keyboard accessibility for inverted message lists: a new KeyboardA11y TurboModule, FocusUtils, native inverted scroll/content views with exit-focus handling and keyboard navigation, MainActivity key remapping when enabled, and React changes to expose/refocus header, composer, and wire native props.

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
Adds KeyboardA11y TurboModule spec and implementation with enable(scope), disable(), and getState() maintaining process-wide enabled/scope.
Main activity key remapping
android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
Overrides dispatchKeyEvent to remap TAB and DPAD navigation when keyboard-a11y is enabled and focused view has an inverted parent; synthesizes and dispatches remapped KeyEvents.
Native inverted scroll & focus utils
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, content view inversion flag and accessibility child ordering, native InvertedScrollView keyboard dispatch to navigate cells and resolve exit via exitFocusNativeId, and new resource @id/tag_inverted_list.
RN native bridge & types
app/lib/native/KeyboardInversionA11yAndroid.ts
Adds typed TurboModule wrapper for KeyboardA11y and helpers: enableRoomViewKeyboardA11y, disableKeyboardA11y, getKeyboardA11yState and KeyboardInversionState type.
RN inverted scroll composition
app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx, app/views/RoomView/List/components/InvertedScrollView.tsx, app/views/RoomView/List/components/List.tsx
Introduces RNLikeInvertedScrollView composing native inverted components, replaces prior JS-only InvertedScrollView, and forwards exitFocusNativeId from FlatList.
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 includes focus(); RoomView conditionally focuses header or composer on open and sets nativeID on composer exit view.
Navigation param & helper
app/stacks/types.ts, app/lib/methods/helpers/goRoom.ts, app/views/RoomsListView/index.tsx
Adds focusHeaderOnOpen?: boolean to RoomView route params, threads it through goRoom(), and passes it from RoomsListView when opening a room.
Gesture / Button / Touch changes
app/containers/Touch.tsx, app/containers/Button/index.tsx, app/containers/Button/Button.test.tsx, patches/react-native-gesture-handler+2.24.0.patch
Swaps between Pressable and RectButton in components, updates prop types and enabled semantics, tweaks tests, and patches RNGH button to support keyboard activation on Apple platforms.
Message accessibility & interactions
app/containers/message/Reactions.tsx, app/containers/message/Thread.tsx, app/containers/message/Touchable.tsx, app/containers/message/index.tsx
Adds accessibility roles/labels/states to reaction/thread controls, replaces wrappers with accessible touchables, introduces keyboard-focusable Pressable, and exposes onThreadPress via MessageContext.
Misc types, exports & deps
app/views/RoomView/*, app/containers/*, package.json
Adds/updates types and props (e.g., exitFocusNativeId, InvertedScrollViewProps), introduces ref forwarding and re-exports for header, and adds dependency react-native-external-keyboard.

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 | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several non-keyboard-navigation changes are present: Button component switched from Pressable to RectButton, Touch component switched from RectButton to Pressable, accessibility metadata added to reactions/threads, and settings view updates. Remove out-of-scope changes (Button, Touch, Reactions accessibility, Thread, SecurityPrivacyView, DisplayPrefsView updates) or create separate PRs to maintain focus on MA-266 keyboard navigation fixes.
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 (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR comprehensively implements MA-266 requirements: adds Android keyboard navigation with proper focus ordering [MainActivity.kt, InvertedScrollView.tsx], keyboard event remapping [KeyboardA11yModule], inverted list support [InvertedScrollContentView], and imperative focus [RoomHeader].
Title check ✅ Passed The title 'feat(a11y): Physical Keyboard Navigation' clearly and specifically describes the main change—adding keyboard navigation accessibility features for physical keyboards. It is concise, uses conventional commit conventions, and directly relates to the core objective of fixing inverted keyboard navigation for accessibility.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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
Copy Markdown

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
Copy Markdown

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
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.70.0.108318

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

@github-actions
Copy link
Copy Markdown

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
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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

Copy link
Copy Markdown
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.

@github-actions
Copy link
Copy Markdown

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
Copy Markdown

iOS Build Available

Rocket.Chat Experimental 4.71.0.108433

Copy link
Copy Markdown
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.

Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@patches/react-native-gesture-handler`+2.24.0.patch:
- Around line 14-21: The _isActivationKeyPress:(NSSet<UIPress *> *)presses
method only checks for UIKeyboardHIDUsageKeyboardSpacebar and
UIKeyboardHIDUsageKeyboardReturnOrEnter; update it to also treat
UIKeyboardHIDUsageKeyboardKeypadEnter as an activation key so external numpad
Enter is recognized. Locate the _isActivationKeyPress implementation and add a
condition that checks for press.key.keyCode ==
UIKeyboardHIDUsageKeyboardKeypadEnter alongside the existing ReturnOrEnter and
Spacebar checks, ensuring the method returns YES when any of these keyCodes are
present.
🪄 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: 1aa59a98-a548-48e6-a82d-7306c8555c65

📥 Commits

Reviewing files that changed from the base of the PR and between 7f3c952 and ac854d3.

📒 Files selected for processing (1)
  • patches/react-native-gesture-handler+2.24.0.patch
📜 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: format
  • GitHub Check: ESLint and Test / run-eslint-and-test
🧰 Additional context used
🧠 Learnings (1)
📚 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:

  • patches/react-native-gesture-handler+2.24.0.patch
🔇 Additional comments (2)
patches/react-native-gesture-handler+2.24.0.patch (2)

9-12: Good focusability guard for disabled/non-interactive states.

This should keep keyboard focus off controls that are not actionable.


28-53: No additional verification needed. The patch correctly adds keyboard support (Space/Return) to RNGestureHandlerButton by synthesizing a single UIControlEventTouchUpInside event per key press. The nested View with accessible and accessibilityRole='button' in BaseButton.tsx is the correct accessibility pattern—it provides screen reader metadata for the BorderlessButton's interactive element, not a separate activation path. Keyboard and accessibility activation will fire the onPress handler once as intended.

Copy link
Copy Markdown
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 (1)
app/views/SecurityPrivacyView.tsx (1)

98-109: Guard against duplicate side-effects when row press and switch change both fire.

If both callbacks execute for one user action, this can double-log events and duplicate AsyncStorage.setItem writes. Consider making toggles idempotent with a quick equality guard.

Proposed hardening diff
 const toggleCrashReport = (value: boolean) => {
+	if (value === crashReportState) {
+		return;
+	}
 	logEvent(events.SP_TOGGLE_CRASH_REPORT);
 	AsyncStorage.setItem(CRASH_REPORT_KEY, JSON.stringify(value));
 	setCrashReportState(value);
 	toggleCrashErrorsReport(value);
 };

 const toggleAnalyticsEvents = (value: boolean) => {
+	if (value === analyticsEventsState) {
+		return;
+	}
 	logEvent(events.SP_TOGGLE_ANALYTICS_EVENTS);
 	AsyncStorage.setItem(ANALYTICS_EVENTS_KEY, JSON.stringify(value));
 	setAnalyticsEventsState(value);
 	toggleAnalyticsEventsReport(value);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/SecurityPrivacyView.tsx` around lines 98 - 109, The row press and
Switch onValueChange handlers (used with analytics toggles and crash report
toggles) can both fire and cause duplicate side-effects; update the toggle
handlers (toggleAnalyticsEvents and toggleCrashReport) to be idempotent by
checking the incoming desired value against the current state
(analyticsEventsState / crashReportState) and returning early if they are equal,
so the AsyncStorage.setItem, logging, and other side-effects only run when the
state actually changes.
🤖 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/views/SecurityPrivacyView.tsx`:
- Around line 98-109: The row press and Switch onValueChange handlers (used with
analytics toggles and crash report toggles) can both fire and cause duplicate
side-effects; update the toggle handlers (toggleAnalyticsEvents and
toggleCrashReport) to be idempotent by checking the incoming desired value
against the current state (analyticsEventsState / crashReportState) and
returning early if they are equal, so the AsyncStorage.setItem, logging, and
other side-effects only run when the state actually changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bb10760b-858b-411f-8128-0e7c978da7d7

📥 Commits

Reviewing files that changed from the base of the PR and between ac854d3 and ef439b8.

📒 Files selected for processing (2)
  • app/views/DisplayPrefsView.tsx
  • app/views/SecurityPrivacyView.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: 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:

  • app/views/SecurityPrivacyView.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/views/SecurityPrivacyView.tsx
🔇 Additional comments (3)
app/views/DisplayPrefsView.tsx (1)

121-121: Nice improvement: row-level toggle for Avatars is now keyboard-friendly.

Adding onPress={toggleAvatar} on Line 121 improves activation from list-item focus and matches the intended accessibility behavior for this settings row.

app/views/SecurityPrivacyView.tsx (2)

98-100: Good accessibility and interaction improvement for analytics toggle.

Making the row pressable and setting the switch role is a solid UX/a11y improvement here.


107-109: Correct state source is now used for crash-report accessibility label.

Switching additionalAccessibilityLabel to crashReportState fixes the prior mismatch and keeps announcements aligned with actual state.

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