Skip to content

refactor: migrate MasterDetailStack to React Navigation static config#7414

Open
diegolmello wants to merge 2 commits into
native-1302-share-extension-static-configfrom
native-1304-master-detail-static-config
Open

refactor: migrate MasterDetailStack to React Navigation static config#7414
diegolmello wants to merge 2 commits into
native-1302-share-extension-static-configfrom
native-1304-master-detail-static-config

Conversation

@diegolmello

@diegolmello diegolmello commented Jun 19, 2026

Copy link
Copy Markdown
Member

Proposed changes

Converts app/stacks/MasterDetailStack/index.tsx from JSX dynamic navigators to React Navigation 7 static config. Four navigators migrated: ChatsStackNavigator, DrawerNavigator, ModalStackNavigator, and the root InsideStack.

Key decisions:

  • ModalStack wraps its Navigator in <ModalContainer> via .with(), calling useNavigation() inside the callback to get the InsideStack navigation for .pop() on backdrop tap.
  • Views that accept a navigation prop are wrapped with withNavigation at the registration site — no view internals changed.
  • isMasterDetail: true is injected in options callbacks for RoomActionsView and ReadReceiptsView, preserving master-detail-specific header behavior.
  • drawerContent: () => <RoomsListView /> expressed in static config via as any cast (DrawerNavigatorProps typing gap).
  • Hand-written param lists in types.ts kept as source of truth; StaticParamList<typeof InsideStack> exported as MasterDetailInsideStaticParamList from index.tsx for future use. Replacing types.ts with re-exports from index.tsx would create a circular type reference since views import from types.ts.

Stacked on #7413 (NATIVE-1302, ShareExtensionStack migration — adds withNavigation HOC used here). Retarget base to develop once #7413 merges.

Issue(s)

https://rocketchat.atlassian.net/browse/NATIVE-1304

How to test or reproduce

On a tablet (or iPad simulator):

  1. Open the app — master-detail split layout renders (drawer + chats pane).
  2. Open a room — RoomView loads in the chats pane.
  3. Open Room Actions (··· menu) — modal stack slides in, header shows master-detail back button (not a full dismiss).
  4. Navigate into Room Info, Members, Search Messages — each screen transitions correctly.
  5. Dismiss modal via backdrop tap — ModalContainer calls navigation.pop() and stack dismisses.
  6. Verify ReadReceiptsView header also shows master-detail style.

Screenshots

N/A — no UI changes, internal navigation wiring only.

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

https://claude.ai/code/session_01Qh9P8Bbp6V7PFpCY2X5on3

Summary by CodeRabbit

  • Refactor
    • Rebuilt the app’s navigation structure to improve screen routing and reduce navigation-related type issues.
    • Updated how screens are composed (including modal and drawer flows) to make transitions more consistent and easier to maintain.

Convert Drawer + ChatsStack + ModalStack + InsideStack from JSX dynamic
navigators to `createNativeStackNavigator`/`createDrawerNavigator` static
config. `ModalStack` wraps its Navigator in `<ModalContainer>` via `.with()`,
using `useNavigation()` to get the InsideStack navigation for `.pop()`.
Views that take a `navigation` prop are wrapped with `withNavigation` at the
registration site; no view internals changed. `isMasterDetail: true` injection
preserved in `RoomActionsView` and `ReadReceiptsView` options callbacks.
Exports `MasterDetailInsideStaticParamList = StaticParamList<typeof InsideStack>`;
hand-written param lists in `types.ts` kept as source of truth to avoid a
circular type reference.

Claude-Session: https://claude.ai/code/session_01Qh9P8Bbp6V7PFpCY2X5on3
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9189c118-592a-4081-882e-66582e45a416

📥 Commits

Reviewing files that changed from the base of the PR and between df36d5f and 4378fc7.

📒 Files selected for processing (1)
  • app/stacks/MasterDetailStack/index.tsx
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/stacks/MasterDetailStack/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript with strict mode enabled

Files:

  • app/stacks/MasterDetailStack/index.tsx
**/*.{js,jsx,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier formatting with tabs, single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses

Files:

  • app/stacks/MasterDetailStack/index.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce ESLint rules from @rocket.chat/eslint-config with React, React Native, TypeScript, and Jest plugins

Files:

  • app/stacks/MasterDetailStack/index.tsx
app/stacks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Place navigation stacks in 'app/stacks/' directory (InsideStack for authenticated, OutsideStack for login/register, MasterDetailStack for tablets, ShareExtensionStack)

Files:

  • app/stacks/MasterDetailStack/index.tsx
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/stacks/MasterDetailStack/index.tsx
🔇 Additional comments (1)
app/stacks/MasterDetailStack/index.tsx (1)

8-8: LGTM!

Also applies to: 87-87, 112-117, 193-193, 236-236, 245-245


Walkthrough

MasterDetailStack/index.tsx is refactored from a memo-based navigator to a typed, hook-based structure. Imports are updated to use StaticScreenProps and withNavigation. Screen components are wrapped with withNavigation as typed ComponentType<StaticScreenProps<...>> constants. Four navigator layers are rebuilt using createNativeStackNavigator().with(): ChatsStack, DrawerNav, ModalStack, and InsideStack. The INavigation interface is removed, and InsideStack.getComponent() replaces the prior default export.

Changes

MasterDetailStack Static Navigator Refactor

Layer / File(s) Summary
Imports and withNavigation screen constants
app/stacks/MasterDetailStack/index.tsx
Imports are updated to use StaticParamList, StaticScreenProps, and withNavigation, removing memo and generic navigation prop types. A large block of ComponentType<StaticScreenProps<...>> constants wraps each screen via withNavigation with as any casts to break recursive navigation type cycles.
ChatsStack and DrawerNav construction
app/stacks/MasterDetailStack/index.tsx
Memo-based ChatsStackNavigator and DrawerNavigator are replaced with createNativeStackNavigator().with() calls that inject ThemeContext screenOptions. ChatsStack hosts RoomView; DrawerNav renders RoomsListView via drawerContent and mounts ChatsStack.
ModalStack reconstruction
app/stacks/MasterDetailStack/index.tsx
ModalStack is rebuilt with createNativeStackNavigator().with() and wrapped in ModalContainer via useNavigation<NativeStackNavigationProp<any>>(). All modal screen routes use the new *ViewScreen constants with navigationOptions callbacks attached to specific routes.
InsideStack root and module exports
app/stacks/MasterDetailStack/index.tsx
The InsideStackNavigator memo is replaced by InsideStack using createNativeStackNavigator().with(), with presentation set based on isIOS. DrawerNav, ModalStack, and inline modal views (attachment, block, jitsi, share, call) are registered. InsideStack.getComponent() is the new default export; INavigation interface is removed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: migrating MasterDetailStack from dynamic JSX navigators to React Navigation 7 static configuration, which matches the core objective and implementation across all four navigators.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (2)
  • NATIVE-1302: Request failed with status code 401
  • NATIVE-1304: Request failed with status code 401

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/stacks/MasterDetailStack/index.tsx (1)

194-241: 💤 Low value

Inconsistent non-null assertions on navigationOptions.

Lines 196 and 231 use non-null assertions (!) on navigationOptions, while lines 206 and 240 access navigationOptions without assertions. This inconsistency could mask potential issues:

// With assertion (lines 196, 231)
options: props => RoomActionsView.navigationOptions!({ ...props, isMasterDetail: true })

// Without assertion (lines 206, 240)
options: SearchMessagesView.navigationOptions

If the assertion is necessary because navigationOptions might be undefined, consider adding it consistently. If it's guaranteed to exist on all these views, the assertions are unnecessary and could be removed for consistency.

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

In `@app/stacks/MasterDetailStack/index.tsx` around lines 194 - 241, The
navigationOptions property access is inconsistent across the screen definitions
in the stack configuration. Some screens like RoomActionsView and
ReadReceiptsView use non-null assertions (!) on navigationOptions, while others
like SearchMessagesView and ScreenLockConfigView access navigationOptions
without assertions. Make this consistent by either adding the non-null assertion
(!) to all navigationOptions accesses across all screens, or removing the
assertions from RoomActionsView and ReadReceiptsView if navigationOptions is
guaranteed to exist on all view types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/stacks/MasterDetailStack/index.tsx`:
- Around line 194-241: The navigationOptions property access is inconsistent
across the screen definitions in the stack configuration. Some screens like
RoomActionsView and ReadReceiptsView use non-null assertions (!) on
navigationOptions, while others like SearchMessagesView and ScreenLockConfigView
access navigationOptions without assertions. Make this consistent by either
adding the non-null assertion (!) to all navigationOptions accesses across all
screens, or removing the assertions from RoomActionsView and ReadReceiptsView if
navigationOptions is guaranteed to exist on all view types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a7d46a68-639f-468f-a23d-5ef168d1e184

📥 Commits

Reviewing files that changed from the base of the PR and between 4cd663c and df36d5f.

📒 Files selected for processing (1)
  • app/stacks/MasterDetailStack/index.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). (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/stacks/MasterDetailStack/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript with strict mode enabled

Files:

  • app/stacks/MasterDetailStack/index.tsx
**/*.{js,jsx,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier formatting with tabs, single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses

Files:

  • app/stacks/MasterDetailStack/index.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce ESLint rules from @rocket.chat/eslint-config with React, React Native, TypeScript, and Jest plugins

Files:

  • app/stacks/MasterDetailStack/index.tsx
app/stacks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Place navigation stacks in 'app/stacks/' directory (InsideStack for authenticated, OutsideStack for login/register, MasterDetailStack for tablets, ShareExtensionStack)

Files:

  • app/stacks/MasterDetailStack/index.tsx
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/stacks/MasterDetailStack/index.tsx
🔇 Additional comments (7)
app/stacks/MasterDetailStack/index.tsx (7)

1-23: LGTM!


75-81: LGTM!


82-161: LGTM!


162-177: LGTM!


179-187: LGTM!


262-272: LGTM!


274-319: LGTM!

@diegolmello diegolmello left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

High-effort code review (NATIVE-1304). Behavior-preserving migration — no correctness bugs, nothing blocking.

Verified sound:

  • ModalStack.with() + useNavigation() resolves to the InsideStack screen navigation (the same object the old ModalStackNavigator received as an injected prop), so navigation.pop() dismisses the modal identically. The riskiest change is correct.
  • isMasterDetail: true preserved verbatim on RoomActionsView and ReadReceiptsView.
  • Screen-name set identical (64) old vs new — nothing dropped or renamed.
  • screenOptions merge-order shift is behavior-neutral (themedHeader sets only headerStyle/headerTintColor/headerTitleStyle).
  • Leaving the hand-written param lists in types.ts is the correct call here — deriving them creates a genuine type cycle (views import the lists from types.ts).

All findings are nits (inline below). One more nit, not line-anchored: class-screen handling is inconsistent — RoomActionsView/SearchMessagesView use withNavigation(...) while ReadReceiptsView/ScreenLockConfigView/RoomInfoView are raw ... as any. Runtime-equivalent (RN injects navigation into every screen; withNavigation is a type adapter), but pick one pattern for consistency with the ShareExtension slice.

On-device tablet smoke test remains the verification gate.

Comment thread app/stacks/MasterDetailStack/index.tsx Outdated
});

export default InsideStackNavigator;
export type MasterDetailInsideStaticParamList = StaticParamList<typeof InsideStack>;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

slop (moderate): MasterDetailInsideStaticParamList has zero consumers repo-wide — dead exported type. In the earlier slices the derived StaticParamList replaced the hand-written list; here it can't, because deriving creates a cycle (types.ts → views → index.tsxStaticParamListtypes.ts). Keeping the hand-written lists is correct, but this standalone export isn't wired to anything and falsely signals the types are derived. Remove it — or, if it's intended as a seed for the AppContainer-root slice, say so in the PR body.

}
// ─── DrawerNavigator ──────────────────────────────────────────────────────────

const DrawerNav = createDrawerNavigator({

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

slop (moderate): casting the entire createDrawerNavigator({...}) config to any drops type-checking on screenOptions, screens, and drawerContent alike. The actual gap is just drawerContent: () => <RoomsListView /> vs DrawerContentComponentProps. Scope the cast to drawerContent so the rest of the config stays checked.

Comment thread app/stacks/MasterDetailStack/index.tsx Outdated
// navigation prop referencing ModalStackParamList ← StaticParamList<typeof ModalStack>
// ← these components. HOC static properties (navigationOptions) are forwarded by
// hoistNonReactStatics inside connect() and withTheme(), so `.navigationOptions`
// is still reachable on the wrapped imports for options callbacks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

nit: "reachable on the wrapped imports" is ambiguous. The options callbacks read the raw connect/withTheme-wrapped default exports (which hoist statics), not the withNavigation wrappers (which don't hoist .navigationOptions). Reword to "connect/withTheme-wrapped default exports" so it doesn't imply the HOC hoists.

Remove the dead exported MasterDetailInsideStaticParamList type (no
consumers; deriving it would create a types cycle). Drop the
createDrawerNavigator config cast so screenOptions/screens/drawerContent
stay type-checked. Wrap ReadReceiptsView and ScreenLockConfigView class
screens with withNavigation to match the other stacks. Clarify that
.navigationOptions is reachable via the connect/withTheme-wrapped default
exports, not the withNavigation wrappers.

Claude-Session: https://claude.ai/code/session_01FambjUT5Y47f8V6Kc9Fpn5
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