Skip to content

fix: prevent thread sidebar from getting stuck#40048

Closed
ZunedKhan07 wants to merge 1 commit intoRocketChat:developfrom
ZunedKhan07:fix-thread-bug
Closed

fix: prevent thread sidebar from getting stuck#40048
ZunedKhan07 wants to merge 1 commit intoRocketChat:developfrom
ZunedKhan07:fix-thread-bug

Conversation

@ZunedKhan07
Copy link
Copy Markdown

@ZunedKhan07 ZunedKhan07 commented Apr 5, 2026

Proposed changes

This PR fixes the issue where clicking on a "Message removed" placeholder opens a thread view that becomes unresponsive and cannot be closed.

Fix Details:

  • Added a condition to prevent opening a thread for deleted messages.
  • Ensured that the thread state resets properly when switching between messages.
  • Prevented stale thread context from persisting across interactions.

Before:

  • Clicking on a deleted message opened a broken thread view.
  • User could not close the thread.
  • Other threads also showed incorrect/stale data.

After:

  • Clicking on a deleted message no longer opens a thread.
  • Thread panel behaves correctly and closes as expected.
  • No stale data persists when switching threads.

Issue(s)

Closes #39908


Steps to test or reproduce

  1. Go to any channel with a deleted message ("Message removed").

  2. Click on the deleted message.

  3. Verify that no thread opens.

  4. Click on a valid message thread.

  5. Switch between multiple threads.

  6. Ensure:

    • Thread opens correctly
    • Thread closes properly
    • No stale content is shown

Further comments

This fix ensures better UI consistency and prevents users from getting stuck in an unresponsive thread view.

The solution avoids unnecessary rendering of thread components for invalid messages and ensures proper cleanup of thread state.

No breaking changes introduced.

Summary by CodeRabbit

  • Bug Fixes
    • Thread tabs now automatically close when the viewed message has been deleted or removed, preventing errors.
    • Enhanced the thread panel's layout and positioning behavior when expanding threads for improved visual consistency.

@ZunedKhan07 ZunedKhan07 requested a review from a team as a code owner April 5, 2026 16:55
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 5, 2026

⚠️ No Changeset found

Latest commit: 2e60f0a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dionisio-bot
Copy link
Copy Markdown
Contributor

dionisio-bot bot commented Apr 5, 2026

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 5, 2026

Walkthrough

Single file modification in the Thread component that removes portal-based rendering, introduces a validation guard for missing or removed messages, and refactors positioning logic for expanded thread display states.

Changes

Cohort / File(s) Summary
Thread Rendering Refactor
apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx
Removed createPortal usage and portal targeting; added early guard to close tab when message is missing or marked as removed (t === 'rm'); refactored positioning logic to use fixed positioning when expanded and absolute otherwise; conditionally render ModalBackdrop only when both canExpand and expanded are true; eliminated intermediate threadContent and portalTarget variables.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

type: bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses thread sidebar responsiveness issues per the PR description, but the linked issue #39908 requests avatar validation in federation-matrix code, which is unrelated to the thread fix. The code changes to Thread.tsx do not implement avatar validation as described in issue #39908. Verify that issue #39908 is the correct linked issue or update the PR to address avatar validation in federation-matrix.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: prevent thread sidebar from getting stuck' directly matches the main objective of fixing an issue where the thread view becomes unresponsive and cannot be closed.
Out of Scope Changes check ✅ Passed All changes are scoped to Thread.tsx and directly address the thread sidebar responsiveness issue described in the PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx">

<violation number="1" location="apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx:48">
P1: Added conditional early return before later hooks breaks hook ordering and performs `closeTab()` side effect during render.</violation>

<violation number="2" location="apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx:109">
P2: Gate fixed positioning with `canExpand` as well, otherwise persisted `expanded` state can force expanded layout when expansion is unavailable.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

mainMessageQueryResult.isSuccess &&
(!mainMessageQueryResult.data || mainMessageQueryResult.data.t === 'rm')
) {
closeTab();
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 5, 2026

Choose a reason for hiding this comment

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

P1: Added conditional early return before later hooks breaks hook ordering and performs closeTab() side effect during render.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx, line 48:

<comment>Added conditional early return before later hooks breaks hook ordering and performs `closeTab()` side effect during render.</comment>

<file context>
@@ -42,6 +41,13 @@ const Thread = ({ tmid }: ThreadProps) => {
+		mainMessageQueryResult.isSuccess &&
+		(!mainMessageQueryResult.data || mainMessageQueryResult.data.t === 'rm')
+	) {
+		closeTab();
+		return null;
+	}
</file context>
Fix with Cubic

`
: undefined
}
position={expanded ? 'fixed' : 'absolute'}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 5, 2026

Choose a reason for hiding this comment

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

P2: Gate fixed positioning with canExpand as well, otherwise persisted expanded state can force expanded layout when expansion is unavailable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx, line 109:

<comment>Gate fixed positioning with `canExpand` as well, otherwise persisted `expanded` state can force expanded layout when expansion is unavailable.</comment>

<file context>
@@ -83,85 +89,64 @@ const Thread = ({ tmid }: ThreadProps) => {
+									`
+								: undefined
+						}
+						position={expanded ? 'fixed' : 'absolute'}
+						display='flex'
+						flexDirection='column'
</file context>
Suggested change
position={expanded ? 'fixed' : 'absolute'}
position={canExpand && expanded ? 'fixed' : 'absolute'}
Fix with Cubic

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: 2

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

Inline comments:
In `@apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx`:
- Around line 95-109: Derive a single boolean isExpanded = canExpand && expanded
and use that everywhere instead of checking expanded alone: use isExpanded for
the ModalBackdrop render condition, for the Box position prop (static vs
relative), and for the Contextualbar position prop (fixed vs absolute) as well
as the className branch so the fixed layout never activates when canExpand is
false; replace occurrences of expanded in these layout branches with isExpanded
to ensure expansion is gated by canExpand.
- Around line 44-50: The early return based on mainMessageQueryResult
short-circuits hook calls and calls closeTab during render; instead derive a
boolean (e.g., shouldCloseDeletedThread = mainMessageQueryResult.isSuccess &&
(!mainMessageQueryResult.data || mainMessageQueryResult.data.t === 'rm')) before
any hooks, then call useTranslation, useToastMessageDispatch,
useLayoutContextualBarExpanded, useLocalStorage, useUserId, and
useToggleFollowingThreadMutation unconditionally; move the closeTab() call into
a useEffect that watches shouldCloseDeletedThread and runs closeTab() when it
becomes true; finally, after the hooks block, conditionally return null if
shouldCloseDeletedThread is true.
🪄 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: 88cdad48-c860-4465-8ff6-cfb99cbd13b0

📥 Commits

Reviewing files that changed from the base of the PR and between 8ebf44b and 2e60f0a.

📒 Files selected for processing (1)
  • apps/meteor/client/views/room/contextualBar/Threads/Thread.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: cubic · AI code reviewer
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

**/*.{ts,tsx,js}: Write concise, technical TypeScript/JavaScript with accurate typing in Playwright tests
Avoid code comments in the implementation

Files:

  • apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx
🧠 Learnings (5)
📓 Common learnings
Learnt from: smirk-dev
Repo: RocketChat/Rocket.Chat PR: 39625
File: apps/meteor/app/api/server/v1/push.ts:85-97
Timestamp: 2026-03-14T14:58:58.834Z
Learning: In RocketChat/Rocket.Chat, the `push.token` POST/DELETE endpoints in `apps/meteor/app/api/server/v1/push.ts` were already migrated to the chained router API pattern on `develop` prior to PR `#39625`. `cleanTokenResult` (which strips `authToken` and returns `PushTokenResult`) and `isPushTokenPOSTProps`/`isPushTokenDELETEProps` validators already exist on `develop`. PR `#39625` only migrates `push.get` and `push.info` to the chained pattern. Do not flag `cleanTokenResult` or `PushTokenResult` as newly introduced behavior-breaking changes when reviewing this PR.
Learnt from: juliajforesti
Repo: RocketChat/Rocket.Chat PR: 38493
File: apps/meteor/tests/e2e/page-objects/fragments/home-content.ts:60-82
Timestamp: 2026-02-24T19:36:55.089Z
Learning: In RocketChat/Rocket.Chat e2e tests (apps/meteor/tests/e2e/page-objects/fragments/home-content.ts), thread message preview listitems do not have aria-roledescription="message", so lastThreadMessagePreview locator cannot be scoped to messageListItems (which filters for aria-roledescription="message"). It should remain scoped to page.getByRole('listitem') or mainMessageList.getByRole('listitem').
Learnt from: ggazzo
Repo: RocketChat/Rocket.Chat PR: 35995
File: apps/meteor/app/api/server/v1/rooms.ts:1107-1112
Timestamp: 2026-02-23T17:53:18.785Z
Learning: In Rocket.Chat PR reviews, maintain strict scope boundaries—when a PR is focused on a specific endpoint (e.g., rooms.favorite), avoid reviewing or suggesting changes to other endpoints that were incidentally refactored (e.g., rooms.invite) unless explicitly requested by maintainers.
📚 Learning: 2026-02-24T19:36:55.089Z
Learnt from: juliajforesti
Repo: RocketChat/Rocket.Chat PR: 38493
File: apps/meteor/tests/e2e/page-objects/fragments/home-content.ts:60-82
Timestamp: 2026-02-24T19:36:55.089Z
Learning: In RocketChat/Rocket.Chat e2e tests (apps/meteor/tests/e2e/page-objects/fragments/home-content.ts), thread message preview listitems do not have aria-roledescription="message", so lastThreadMessagePreview locator cannot be scoped to messageListItems (which filters for aria-roledescription="message"). It should remain scoped to page.getByRole('listitem') or mainMessageList.getByRole('listitem').

Applied to files:

  • apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx
📚 Learning: 2026-03-11T22:04:20.529Z
Learnt from: juliajforesti
Repo: RocketChat/Rocket.Chat PR: 39545
File: apps/meteor/client/views/room/body/hooks/useHasNewMessages.ts:59-61
Timestamp: 2026-03-11T22:04:20.529Z
Learning: In `apps/meteor/client/views/room/body/hooks/useHasNewMessages.ts`, the `msg.u._id === uid` early-return in the `streamNewMessage` handler is intentional: the "New messages" indicator is designed to notify about messages from other users only. Self-sent messages — including those sent from a different session/device — are always skipped, by design. Do not flag this as a multi-session regression.

Applied to files:

  • apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx
📚 Learning: 2026-03-04T14:16:49.202Z
Learnt from: tassoevan
Repo: RocketChat/Rocket.Chat PR: 39304
File: packages/ui-contexts/src/ActionManagerContext.ts:26-26
Timestamp: 2026-03-04T14:16:49.202Z
Learning: In `packages/ui-contexts/src/ActionManagerContext.ts` (TypeScript, RocketChat/Rocket.Chat), the `disposeView` method in `IActionManager` uses an intentionally explicit union `UiKit.ModalView['id'] | UiKit.BannerView['viewId'] | UiKit.ContextualBarView['id']` to document which view types are accepted, even though all constituents resolve to the same primitive. The inline `// eslint-disable-next-line typescript-eslint/no-duplicate-type-constituents` comment is intentional and should not be flagged or removed.

Applied to files:

  • apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx
📚 Learning: 2026-03-27T14:52:56.865Z
Learnt from: dougfabris
Repo: RocketChat/Rocket.Chat PR: 39892
File: apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx:150-155
Timestamp: 2026-03-27T14:52:56.865Z
Learning: In Rocket.Chat, there are two different `ModalBackdrop` components with different prop APIs. During review, confirm the import source: (1) `rocket.chat/fuselage` `ModalBackdrop` uses `ModalBackdropProps` based on `BoxProps` (so it supports `onClick` and other Box/DOM props) and does not have an `onDismiss` prop; (2) `rocket.chat/ui-client` `ModalBackdrop` uses a narrower props interface like `{ children?: ReactNode; onDismiss?: () => void }` and handles Escape keypress and outside mouse-up, and it does not forward arbitrary DOM props such as `onClick`. Flag mismatched props (e.g., `onDismiss` passed to the fuselage component or `onClick` passed to the ui-client component) and ensure the usage matches the correct component being imported.

Applied to files:

  • apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx

Comment on lines +44 to +50
if (
mainMessageQueryResult.isSuccess &&
(!mainMessageQueryResult.data || mainMessageQueryResult.data.t === 'rm')
) {
closeTab();
return null;
}
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

React documentation: can a component conditionally return before hooks that were called on a previous render, and are side effects such as state updates/navigation allowed during render?

💡 Result:

No, a React component cannot conditionally return before hooks that were called on a previous render. Hooks must be called at the top level of the component, in the same order on every render, before any early returns. A conditional early return before all hooks would violate the Rules of Hooks, causing React to lose track of hook state and leading to bugs or errors like "Rendered fewer hooks than expected." Side effects such as state updates (e.g., setState) or navigation during render are not allowed. Render must be a pure function that computes and returns JSX without mutating state, DOM, or performing I/O. Such side effects belong in event handlers or useEffect/useLayoutEffect. Workarounds: - For conditional logic before hooks: Split into an outer component that checks the condition and returns null or renders an inner component containing the hooks. - For side effects: Move them to useEffect (for post-render) or event handlers.

Citations:


🏁 Script executed:

head -100 apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx | tail -60

Repository: RocketChat/Rocket.Chat

Length of output: 1481


Move this deleted-message close path below the hook declarations.

The conditional check at lines 44–50 short-circuits before useTranslation, useToastMessageDispatch, useLayoutContextualBarExpanded, useLocalStorage, useUserId, and useToggleFollowingThreadMutation. This violates React's Rules of Hooks—if the query state changes on a later render, the component will call fewer hooks than before, causing React to lose track of hook state. Additionally, line 48 performs a render-phase side effect by calling closeTab(), which is not allowed.

Derive a shouldCloseDeletedThread flag from the query result, call all hooks unconditionally, move the side effect to a useEffect, and conditionally return null after the hook block.

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

In `@apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx` around lines
44 - 50, The early return based on mainMessageQueryResult short-circuits hook
calls and calls closeTab during render; instead derive a boolean (e.g.,
shouldCloseDeletedThread = mainMessageQueryResult.isSuccess &&
(!mainMessageQueryResult.data || mainMessageQueryResult.data.t === 'rm')) before
any hooks, then call useTranslation, useToastMessageDispatch,
useLayoutContextualBarExpanded, useLocalStorage, useUserId, and
useToggleFollowingThreadMutation unconditionally; move the closeTab() call into
a useEffect that watches shouldCloseDeletedThread and runs closeTab() when it
becomes true; finally, after the hooks block, conditionally return null if
shouldCloseDeletedThread is true.

Comment on lines +95 to +109
{canExpand && expanded && <ModalBackdrop onClick={handleBackdropClick} />}
<Box flexGrow={1} position={expanded ? 'static' : 'relative'}>
<Contextualbar
rcx-thread-view
className={
canExpand && expanded
? css`
max-width: 855px !important;
@media (min-width: 780px) and (max-width: 1135px) {
max-width: calc(100% - var(--sidebar-width)) !important;
}
`
: undefined
}
position={expanded ? 'fixed' : 'absolute'}
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.

⚠️ Potential issue | 🟡 Minor

Gate the fixed positioning with canExpand too.

Line 95 uses canExpand && expanded, but Lines 96 and 109 still switch the layout to static/fixed when expanded alone is true. Because expanded is persisted in local storage, reopening the thread in a layout where expansion is unavailable can still render the fixed view without its backdrop or expand/collapse affordance. Derive a single isExpanded = canExpand && expanded and use it for all layout branches.

💡 Suggested fix
 const canExpand = useLayoutContextualBarExpanded();
 const [expanded, setExpanded] = useLocalStorage('expand-threads', false);
+const isExpanded = canExpand && expanded;
 
 ...
-				{canExpand && expanded && <ModalBackdrop onClick={handleBackdropClick} />}
-				<Box flexGrow={1} position={expanded ? 'static' : 'relative'}>
+				{isExpanded && <ModalBackdrop onClick={handleBackdropClick} />}
+				<Box flexGrow={1} position={isExpanded ? 'static' : 'relative'}>
 					<Contextualbar
 						rcx-thread-view
 						className={
-							canExpand && expanded
+							isExpanded
 								? css`
 										max-width: 855px !important;
 										`@media` (min-width: 780px) and (max-width: 1135px) {
 											max-width: calc(100% - var(--sidebar-width)) !important;
 										}
 									`
 								: undefined
 						}
-						position={expanded ? 'fixed' : 'absolute'}
+						position={isExpanded ? 'fixed' : 'absolute'}
 						display='flex'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx` around lines
95 - 109, Derive a single boolean isExpanded = canExpand && expanded and use
that everywhere instead of checking expanded alone: use isExpanded for the
ModalBackdrop render condition, for the Box position prop (static vs relative),
and for the Contextualbar position prop (fixed vs absolute) as well as the
className branch so the fixed layout never activates when canExpand is false;
replace occurrences of expanded in these layout branches with isExpanded to
ensure expansion is gated by canExpand.

@dougfabris
Copy link
Copy Markdown
Member

Hi there, thanks for the contribution! 🚀 💯

Closing this because the changes doesn't reflect the description of the pull request so that's an invalid contribution.


Questions? Help needed? Feature Requests?

  • Join our Open Server in the #support channel and feel free to raise a question
  • Join our Community Forum and search/create a post there

@dougfabris dougfabris closed this Apr 5, 2026
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.

need to perform a validation to check if the user actually changed avatar

2 participants