Skip to content

fix: make reaction tab bar horizontally scrollable when emoji count overflows#7222

Open
divyanshu-patil wants to merge 3 commits intoRocketChat:developfrom
divyanshu-patil:fix/emojitab
Open

fix: make reaction tab bar horizontally scrollable when emoji count overflows#7222
divyanshu-patil wants to merge 3 commits intoRocketChat:developfrom
divyanshu-patil:fix/emojitab

Conversation

@divyanshu-patil
Copy link
Copy Markdown

@divyanshu-patil divyanshu-patil commented Apr 23, 2026

Proposed changes

The reaction tab bar in the message reactions bottom sheet was not scrollable, causing emoji tabs to overflow and clip off-screen when a message had many distinct reactions. This PR fixes the layout by wrapping the tab items in a horizontal ScrollView inside the existing View container, ensuring all reaction tabs are accessible regardless of count.

Issue(s)

emoji tabs to overflow and clip off-screen when a message had many distinct reactions.

How to test or reproduce

  1. Open a message that has many distinct emoji reactions (enough to overflow the screen width)
  2. Observe the reaction tab bar — tabs were previously clipped with no way to scroll
  3. After this fix, the tab bar scrolls horizontally and all reaction tabs are reachable
  4. Tap each tab and verify the reaction list filters correctly
  5. Also verify that with few reactions, the layout looks normal with no blank space between the tab bar and the list

Screenshots

Before After
image image

With fewer reactions
image

Recording

reaction.tabbar.mp4

Note

the empty userlist for a reaction shown in video is a bug solved in #7204 and with server change RocketChat/Rocket.Chat#40254

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

The outer View wrapper is intentionally kept around the ScrollView — replacing the top-level element with a ScrollView affecting the layout by adding huge space at the bottom. Wrapping preserves layout while still enabling horizontal scroll. The ScrollView is imported from react-native-gesture-handler to ensure correct gesture handling on both iOS and Android.

Summary by CodeRabbit

Release Notes

  • New Features

    • Tab navigation now supports horizontal scrolling, allowing access to more tabs without expanding the interface.
  • Improvements

    • Enhanced tab spacing and layout for better visual presentation and usability.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Walkthrough

The TabView component is modified to render tabs within a horizontally scrollable container instead of a fixed row layout. The ScrollView component wraps the mapped tab items, and styling adjustments remove flex-based sizing in favor of fixed padding for individual tabs.

Changes

Cohort / File(s) Summary
TabView Implementation
app/containers/TabView/index.tsx
Wrapped tab items in ScrollView with horizontal prop and disabled scroll indicators; updated imports to include ScrollView from react-native-gesture-handler.
TabView Styling
app/containers/TabView/styles.ts
Removed flexDirection: 'row' from tabsContainer and flex: 1 from individual tab styles; added paddingHorizontal: 8 to tab styling.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

type: bug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: making the reaction tab bar horizontally scrollable to handle emoji overflow, which matches the core modification shown in both file changes.
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.
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.

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/containers/TabView/index.tsx (1)

29-43: Minor: active tab is not scrolled into view when it changes.

With a scrollable tab bar, if the current tab (routeIndex) is off‑screen — e.g. after jumpTo(...) is called programmatically, or when the component mounts with a non‑zero index — the user won't see which tab is selected until they scroll. Consider measuring the selected tab and calling scrollTo({ x, animated: true }) via a ScrollView ref whenever routeIndex changes, or at minimum on mount.

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

In `@app/containers/TabView/index.tsx` around lines 29 - 43, The scrollable tab
bar doesn't auto-scroll the active tab into view; add a ref to the ScrollView
(the ScrollView wrapping routes.map) and ensure each tab View (currently keyed
by tab.key and using styles.tab) can be measured (either by storing refs for
each tab View or using measureLayout). On mount and whenever routeIndex changes
(the prop used to decide active styling and passed to renderTabItem/jumpTo),
compute the x offset of the active tab and call scrollViewRef.current.scrollTo({
x, animated: true }) so the selected tab is brought into view; perform this
logic after layout (useEffect/useLayoutEffect) and guard for null refs. Ensure
this also runs when jumpTo is called programmatically.
🤖 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/TabView/index.tsx`:
- Around line 29-43: The scrollable tab bar doesn't auto-scroll the active tab
into view; add a ref to the ScrollView (the ScrollView wrapping routes.map) and
ensure each tab View (currently keyed by tab.key and using styles.tab) can be
measured (either by storing refs for each tab View or using measureLayout). On
mount and whenever routeIndex changes (the prop used to decide active styling
and passed to renderTabItem/jumpTo), compute the x offset of the active tab and
call scrollViewRef.current.scrollTo({ x, animated: true }) so the selected tab
is brought into view; perform this logic after layout
(useEffect/useLayoutEffect) and guard for null refs. Ensure this also runs when
jumpTo is called programmatically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f1aaa625-9a4e-4a09-a09a-40774f7d9db0

📥 Commits

Reviewing files that changed from the base of the PR and between f116acc and 89b4eb3.

⛔ Files ignored due to path filters (1)
  • app/containers/MessageComposer/__snapshots__/MessageComposer.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • app/containers/TabView/index.tsx
  • app/containers/TabView/styles.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/containers/TabView/styles.ts
  • app/containers/TabView/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

**/*.{ts,tsx}: Use TypeScript with strict mode enabled and baseUrl set to app/ for module imports
Support iOS 13.4+ and Android 6.0+ as minimum target platforms

Files:

  • app/containers/TabView/styles.ts
  • app/containers/TabView/index.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use tabs for indentation with single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses when possible
Use ESLint with @rocket.chat/eslint-config base including React, React Native, TypeScript, and Jest plugins

Files:

  • app/containers/TabView/styles.ts
  • app/containers/TabView/index.tsx
app/containers/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Create reusable UI components in app/containers/ directory

Files:

  • app/containers/TabView/styles.ts
  • app/containers/TabView/index.tsx
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T22:57:58.545Z
Learning: Applies to app/lib/hooks/useResponsiveLayout/**/*.{ts,tsx} : Implement responsive layouts using useResponsiveLayout hook to switch between master-detail on tablets and single stack on phones
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T22:57:58.545Z
Learning: Applies to app/AppContainer.tsx : Use AppContainer.tsx as the root navigation container that switches between authentication states
📚 Learning: 2026-04-22T22:57:58.545Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T22:57:58.545Z
Learning: Applies to app/containers/**/*.{ts,tsx} : Create reusable UI components in app/containers/ directory

Applied to files:

  • app/containers/TabView/styles.ts
  • app/containers/TabView/index.tsx
📚 Learning: 2026-04-22T22:57:58.545Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T22:57:58.545Z
Learning: Applies to app/lib/hooks/useResponsiveLayout/**/*.{ts,tsx} : Implement responsive layouts using useResponsiveLayout hook to switch between master-detail on tablets and single stack on phones

Applied to files:

  • app/containers/TabView/styles.ts
  • app/containers/TabView/index.tsx
📚 Learning: 2026-04-22T22:57:58.545Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T22:57:58.545Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use tabs for indentation with single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses when possible

Applied to files:

  • app/containers/TabView/styles.ts
📚 Learning: 2026-04-22T22:57:58.545Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T22:57:58.545Z
Learning: Applies to app/AppContainer.tsx : Use AppContainer.tsx as the root navigation container that switches between authentication states

Applied to files:

  • app/containers/TabView/index.tsx
📚 Learning: 2026-04-22T22:57:58.545Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T22:57:58.545Z
Learning: Applies to app/index.tsx : Set up Redux provider, theme, navigation, and notifications in app/index.tsx

Applied to files:

  • app/containers/TabView/index.tsx
🔇 Additional comments (2)
app/containers/TabView/styles.ts (1)

4-11: LGTM — styles are consistent with the new scrollable row.

Dropping flexDirection: 'row' on tabsContainer (the inner ScrollView handles row layout) and replacing flex: 1 with paddingHorizontal: 8 on tab is the right pairing for the change in index.tsx. tabLine: { width: '100%' } remains valid because it's relative to each now content‑sized tab.

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

26-47: EmojiPicker's flex: 1 tab style is not applied to TabView and does not regress.

The shared TabView container has two consumers: EmojiPicker (9 fixed emoji category tabs) and ReactionsList (variable reaction count tabs). The concern about EmojiPicker's tab distribution is unfounded—the flex: 1 style defined in app/containers/EmojiPicker/styles.ts is not applied to the TabView tabs. TabView uses its own styles.tab from app/containers/TabView/styles.ts, which sizes tabs to content width via paddingHorizontal: 8. For EmojiPicker's small icon tabs (24px), content-width sizing is acceptable.

ReactionsList benefits from the scrollable tab bar when displaying many reactions. The MIN_TAB_WIDTH = 70 constant defined in ReactionsList styles is unused and should be removed if no minimum enforced.

No regression detected for either consumer.

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