Skip to content

fix: persist attachment collapsed state across channel switches#40365

Open
SYED-MHAMIL wants to merge 2 commits intoRocketChat:developfrom
SYED-MHAMIL:fix/persistant-collapse-attachment
Open

fix: persist attachment collapsed state across channel switches#40365
SYED-MHAMIL wants to merge 2 commits intoRocketChat:developfrom
SYED-MHAMIL:fix/persistant-collapse-attachment

Conversation

@SYED-MHAMIL
Copy link
Copy Markdown

@SYED-MHAMIL SYED-MHAMIL commented May 3, 2026

Proposed changes

Message attachments (images, GIFs, videos, audio, files) store their collapsed/expanded state only in React component state. When a user switches channels, the message list unmounts and all in-memory state is lost — so every manually collapsed attachment re-expands on return.

This fix replaces the in-memory toggle in useCollapse with useState backed by sessionStorage, keyed on the attachment ID. On mount the hook reads the stored value; every toggle writes it back. The user's collapse choice now survives channel switches for the duration of the browser session.

Files changed:

  • useCollapse.tsx — Added usePersistedCollapse helper; updated hook to accept attachmentId parameter and use sessionStorage
  • MessageCollapsible.tsx — Added attachmentId prop, passes it to useCollapse
  • ImageAttachment.tsx — Destructures id and passes as attachmentId to MessageCollapsible
  • VideoAttachment.tsx — Destructures id and passes as attachmentId to MessageCollapsible
  • AudioAttachment.tsx — Destructures id and passes as attachmentId to MessageCollapsible
  • GenericFileAttachment.tsx — Destructures id and passes as attachmentId to MessageCollapsible

Issue(s)

Closes #40337

Steps to test or reproduce

  1. Post a message with an image, GIF, video, audio, or file attachment in any channel
  2. Click the collapse arrow on the attachment, it hides
  3. Switch to another channel, then switch back to the original channel
  4. Before: attachment re-expands
    After: attachment stays collapsed

Further comments

  • sessionStorage was chosen over localStorage so the state does not persist across browser sessions or bleed between different users on the same machine
  • The fix is backward compatible attachmentId is optional; components without IDs fall back to regular toggle behavior
  • No external dependencies added; uses native browser sessionStorage API

before fix: https://github.com/user-attachments/assets/44c08ca8-9f5b-418f-9339-3627bcf1afa4
after fix: https://github.com/user-attachments/assets/e2d14435-d68d-4272-be10-17fb577711a0

Summary by CodeRabbit

  • New Features
    • Attachment collapse state is now persisted during your session. When you expand or collapse audio, video, image, or file attachments, their state will be remembered as you continue browsing.

@SYED-MHAMIL SYED-MHAMIL requested a review from a team as a code owner May 3, 2026 09:33
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 3, 2026

⚠️ No Changeset found

Latest commit: f664fa5

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 May 3, 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 May 3, 2026

Walkthrough

The PR adds attachment-level collapse state persistence to sessionStorage by threading an attachmentId through the component hierarchy, updating the useCollapse hook to store and retrieve expanded/collapsed state per-attachment, and propagating the attachment ID from image, audio, video, and generic file attachment components into the MessageCollapsible component.

Changes

Attachment Collapse State Persistence

Layer / File(s) Summary
Props Enhancement
apps/meteor/client/components/message/MessageCollapsible.tsx
MessageCollapsibleProps adds optional attachmentId?: string field; component destructures it and passes to useCollapse.
Core Persistence Logic
apps/meteor/client/components/message/hooks/useCollapse.tsx
useCollapse signature updated to accept attachmentId. New internal usePersistedCollapse helper stores/retrieves collapsed state from sessionStorage with key persistant-attachment-${attachmentId}; state syncs via useEffect on attachmentId changes.
Component Integration
apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx, GenericFileAttachment.tsx, ImageAttachment.tsx, VideoAttachment.tsx
Each attachment component now destructures id from props and forwards attachmentId={id} to MessageCollapsible, enabling per-attachment state tracking.
Formatting
apps/meteor/client/components/message/content/urlPreviews/OEmbedCollapsible.tsx
Whitespace adjustment in MessageCollapsible JSX opening tag (trailing space added).

Sequence Diagram

sequenceDiagram
    participant User as User
    participant AudioAtt as AudioAttachment<br/>(or other file type)
    participant MsgColl as MessageCollapsible
    participant UseCollapse as useCollapse Hook
    participant Storage as sessionStorage

    User->>AudioAtt: Render attachment (id: "abc123")
    AudioAtt->>MsgColl: Render with attachmentId="abc123"
    MsgColl->>UseCollapse: Call useCollapse("abc123", initialCollapsed)
    UseCollapse->>Storage: Read value from key "persistant-attachment-abc123"
    Storage-->>UseCollapse: Return stored collapsed state (or undefined)
    UseCollapse-->>MsgColl: Return [collapsed, toggle] + CollapsibleContent
    MsgColl-->>AudioAtt: Render collapsible with toggle button
    
    User->>MsgColl: Click collapse/expand button
    MsgColl->>UseCollapse: Call toggle() handler
    UseCollapse->>UseCollapse: Update local collapsed state
    UseCollapse->>Storage: Write new state to "persistant-attachment-abc123"
    Storage-->>UseCollapse: Write complete
    
    User->>User: Switch to another channel
    User->>User: Switch back to original channel
    AudioAtt->>MsgColl: Re-render attachment (same id: "abc123")
    MsgColl->>UseCollapse: Call useCollapse("abc123", initialCollapsed)
    UseCollapse->>Storage: Read value from "persistant-attachment-abc123"
    Storage-->>UseCollapse: Return previously stored state
    UseCollapse-->>MsgColl: Return [collapsed, toggle] with persisted state
    MsgColl-->>User: Render attachment in saved collapse state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested labels

type: bug

🚥 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 clearly and concisely summarizes the main objective: persisting attachment collapsed state across channel switches, which directly matches the core changes throughout the PR.
Linked Issues check ✅ Passed All requirements from issue #40337 are met: attachment collapse state persists across channel switches using sessionStorage keyed by attachment ID, backward compatibility is maintained with optional attachmentId, and session-scoped storage prevents cross-user state bleed.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue: useCollapse receives attachmentId and uses sessionStorage, MessageCollapsible accepts and forwards attachmentId, and all file attachment components destructure id and pass it to MessageCollapsible with no unrelated modifications.
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.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
Review rate limit: 6/8 reviews remaining, refill in 12 minutes and 30 seconds.

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.

No issues found across 7 files

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/components/message/hooks/useCollapse.tsx`:
- Line 52: The debug console.log in the useCollapse hook is printing "reset
state" on every render; remove that console.log('reset state') from the hook
(or, if you want to keep it for local debugging only, guard it behind a
development-only check such as NODE_ENV or __DEV__) so the hook (useCollapse) no
longer emits render-time logs in production.
- Around line 35-42: When attachmentId changes the useEffect currently only
calls setCollapsed if sessionStorage has a value, so the previous attachment's
state can leak; update the effect in useEffect that reads
`persistant-attachment-${attachmentId}` to call setCollapsed(initialCollapsed)
when stored is null (i.e., no saved entry) so the new attachment resets to the
default; ensure you reference `attachmentId`, `setCollapsed`, and
`initialCollapsed` inside that effect (and add `initialCollapsed` to the
dependency array if it isn’t already).
🪄 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: 4f2b84ea-8cc8-46d1-834b-8c4dd5aa9df2

📥 Commits

Reviewing files that changed from the base of the PR and between d33009a and f664fa5.

📒 Files selected for processing (7)
  • apps/meteor/client/components/message/MessageCollapsible.tsx
  • apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx
  • apps/meteor/client/components/message/content/urlPreviews/OEmbedCollapsible.tsx
  • apps/meteor/client/components/message/hooks/useCollapse.tsx
📜 Review details
🧰 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/components/message/content/attachments/file/ImageAttachment.tsx
  • apps/meteor/client/components/message/content/urlPreviews/OEmbedCollapsible.tsx
  • apps/meteor/client/components/message/MessageCollapsible.tsx
  • apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx
  • apps/meteor/client/components/message/hooks/useCollapse.tsx
🧠 Learnings (1)
📚 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/components/message/content/attachments/file/ImageAttachment.tsx
  • apps/meteor/client/components/message/content/urlPreviews/OEmbedCollapsible.tsx
  • apps/meteor/client/components/message/MessageCollapsible.tsx
  • apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx
  • apps/meteor/client/components/message/hooks/useCollapse.tsx
🔇 Additional comments (6)
apps/meteor/client/components/message/MessageCollapsible.tsx (1)

18-19: Looks good.

Forwarding attachmentId into useCollapse matches the new persisted-collapse contract cleanly, and the wrapper still falls back to the existing behavior when no ID is present.

apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx (1)

28-39: Looks good.

Passing the attachment id through here is the right hook-up for per-attachment persistence, and the existing image rendering path is unchanged.

apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx (1)

70-87: Looks good.

Forwarding attachmentId={id} aligns this wrapper with the persisted-collapse behavior, and the download/preview logic remains intact.

apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx (1)

23-27: Looks good.

This forwards the attachment identifier into the persisted-collapse path without changing the audio playback behavior.

apps/meteor/client/components/message/content/urlPreviews/OEmbedCollapsible.tsx (1)

14-20: No concerns.

This is formatting-only and does not change the collapse behavior or props passed to MessageCollapsible.

apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx (1)

24-32: Looks good.

Threading attachmentId through here is consistent with the persisted-collapse update, and the video rendering path remains unchanged.

Comment on lines +35 to +42
useEffect(() => {
if (attachmentId) {
const stored = sessionStorage.getItem(`persistant-attachment-${attachmentId}`);
if (stored !== null) {
setCollapsed(stored === 'true');
}
}
}, [attachmentId]);
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 | 🟠 Major | ⚡ Quick win

Reset to the new attachment's default when storage is empty.

When attachmentId changes, this effect only updates state if sessionStorage already has a value. If the new attachment has no saved entry yet, the hook keeps the previous attachment’s collapsed state instead of reinitializing from initialCollapsed.

🔧 Proposed fix
 useEffect(() => {
 	if (attachmentId) {
 		const stored = sessionStorage.getItem(`persistant-attachment-${attachmentId}`);
-		if (stored !== null) {
-			setCollapsed(stored === 'true');
-		}
+		setCollapsed(stored === null ? initialCollapsed : stored === 'true');
 	}
-}, [attachmentId]);
+}, [attachmentId, initialCollapsed]);
📝 Committable suggestion

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

Suggested change
useEffect(() => {
if (attachmentId) {
const stored = sessionStorage.getItem(`persistant-attachment-${attachmentId}`);
if (stored !== null) {
setCollapsed(stored === 'true');
}
}
}, [attachmentId]);
useEffect(() => {
if (attachmentId) {
const stored = sessionStorage.getItem(`persistant-attachment-${attachmentId}`);
setCollapsed(stored === null ? initialCollapsed : stored === 'true');
}
}, [attachmentId, initialCollapsed]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/client/components/message/hooks/useCollapse.tsx` around lines 35
- 42, When attachmentId changes the useEffect currently only calls setCollapsed
if sessionStorage has a value, so the previous attachment's state can leak;
update the effect in useEffect that reads
`persistant-attachment-${attachmentId}` to call setCollapsed(initialCollapsed)
when stored is null (i.e., no saved entry) so the new attachment resets to the
default; ensure you reference `attachmentId`, `setCollapsed`, and
`initialCollapsed` inside that effect (and add `initialCollapsed` to the
dependency array if it isn’t already).


export const useCollapse = (attachmentId : string | undefined,attachmentCollapsed?: boolean): [collapsed: boolean, node: ReactNode] => {

console.log('reset state')
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 | ⚡ Quick win

Remove the render-time debug log.

console.log('reset state') runs on every render and will leak noise into production consoles.

🧹 Proposed fix
-	console.log('reset state')
📝 Committable suggestion

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

Suggested change
console.log('reset state')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/client/components/message/hooks/useCollapse.tsx` at line 52, The
debug console.log in the useCollapse hook is printing "reset state" on every
render; remove that console.log('reset state') from the hook (or, if you want to
keep it for local debugging only, guard it behind a development-only check such
as NODE_ENV or __DEV__) so the hook (useCollapse) no longer emits render-time
logs in production.

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.

Attachment collapse state is not persisted when switching channels

1 participant