Skip to content

fix(livechat): clean up widget teardown lifecycle#38745

Open
Shreyas2004wagh wants to merge 2 commits intoRocketChat:developfrom
Shreyas2004wagh:fix/livechat-widget-cleanup
Open

fix(livechat): clean up widget teardown lifecycle#38745
Shreyas2004wagh wants to merge 2 commits intoRocketChat:developfrom
Shreyas2004wagh:fix/livechat-widget-cleanup

Conversation

@Shreyas2004wagh
Copy link
Contributor

@Shreyas2004wagh Shreyas2004wagh commented Feb 17, 2026

Summary

  • add explicit teardown for livechat widget listeners/intervals in legacy widget asset and TS widget implementation
  • prevent duplicate navigation/message/media-query listeners across re-inits
  • fix mobile teardown behavior to restore body scroll/styles only when the widget is open

Validation

  • local regression check executed for mobile flow: open -> close -> scroll -> remove (passes)
  • local regression check executed for remove while open restoring original scroll (passes)

Notes

  • local test script was temporary and removed (not included in this PR)

Fix #38746

Summary by CodeRabbit

  • Bug Fixes
    • More reliable widget lifecycle with full cleanup when closing or navigating pages.
    • Prevents duplicate initialization and navigation tracking intervals.
    • Ensures message listeners and responsive behavior are attached/removed cleanly across browsers.
    • Restores scroll and layout state when the widget is removed to avoid UI glitches.

Copilot AI review requested due to automatic review settings February 17, 2026 10:50
@Shreyas2004wagh Shreyas2004wagh requested review from a team as code owners February 17, 2026 10:50
@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Feb 17, 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

@changeset-bot
Copy link

changeset-bot bot commented Feb 17, 2026

⚠️ No Changeset found

Latest commit: a2eb3ac

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

Walkthrough

Adds comprehensive widget lifecycle and teardown logic: stored listener callbacks, media-query listener handling, guarded navigation interval tracking, and teardown routines that remove listeners, stop intervals, restore DOM/scroll state, and prevent duplicate initialization on re-initialization.

Changes

Cohort / File(s) Summary
Legacy widget bundle
apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js
Introduce stored message listener and teardown helpers (detachMessageListener, stopNavigationTracking, detachMediaQueryListener, teardownWidget); replace direct DOM removal with teardown; guard navigation interval creation; attach/detach media-query listeners with cross-browser checks.
Modern widget implementation
packages/livechat/src/widget.ts
Add per-event callback management (callbackHandlers) with correct registration/unregistration, module-scoped navigationIntervalId/message/mediaQuery refs, attach/detach helpers, guarded trackNavigation, teardownWidget that stops intervals, detaches listeners, restores DOM/scroll, and prevents leaks on re-init.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰
I hopped in quick to mend the leaks,
Unhooked the bells and slowed the ticks,
Cleared every thread and swept the floor,
Now the widget sleeps and leaks no more. 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(livechat): clean up widget teardown lifecycle' clearly and concisely describes the main change: fixing lifecycle cleanup in the livechat widget teardown process.
Linked Issues check ✅ Passed All linked issue #38746 objectives are met: lifecycle cleanup for listeners/intervals is implemented in both widget.ts and rocket-livechat.js, clearAllCallbacks() now properly unregisters handlers, and re-initializations call teardownWidget to prevent listener accumulation.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the widget teardown lifecycle as specified in issue #38746; no unrelated modifications were detected in the changeset.

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

@Shreyas2004wagh Shreyas2004wagh force-pushed the fix/livechat-widget-cleanup branch from 2e10515 to 14bc9fc Compare February 17, 2026 10:53
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Cleans up the livechat widget teardown lifecycle to prevent listener/interval leaks across re-inits, across both the legacy injected asset and the TypeScript widget implementation.

Changes:

  • Added explicit teardown helpers to stop navigation polling and detach message + matchMedia listeners.
  • Prevented duplicate listener/interval attachment on repeated init() calls by guarding and tearing down prior instances.
  • Updated removeWidget() to run full teardown instead of only removing the DOM node.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/livechat/src/widget.ts Adds tracked listener/interval management and a centralized teardownWidget() used on removeWidget() and re-init.
apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js Mirrors teardown behavior in the legacy widget asset, tracking listener fns/intervals so they can be detached on teardown.
Comments suppressed due to low confidence (1)

packages/livechat/src/widget.ts:795

  • On re-init, trackNavigation() decides whether to send the initial pageVisited() messages based on currentPage.href/title, but those values aren't reset during teardown. After a teardownWidget() + init() on the same page/title, the new iframe session may never receive an initial pageVisited event until the URL/title changes. Consider resetting currentPage.href/currentPage.title (e.g., in teardownWidget() or before starting navigation tracking).
const trackNavigation = () => {
	if (navigationIntervalId !== null) {
		return;
	}

	navigationIntervalId = setInterval(() => {
		if (document.location.href !== currentPage.href) {
			pageVisited('url');
			currentPage.href = document.location.href;
		}

		if (document.title !== currentPage.title) {
			pageVisited('title');
			currentPage.title = document.title;
		}
	}, 800);
};

const init = (url: string) => {
	const trimmedUrl = url.trim();
	if (!trimmedUrl) {
		return;
	}

	if (widget) {
		teardownWidget();
	}

	config.url = trimmedUrl;

	createWidget(trimmedUrl);
	attachMessageListener();
	trackNavigation();
};

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
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 2 files

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

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 508b4a1 and 14bc9fc.

📒 Files selected for processing (2)
  • apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js
  • packages/livechat/src/widget.ts
🧰 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:

  • packages/livechat/src/widget.ts
  • apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js
🧠 Learnings (1)
📚 Learning: 2025-11-19T18:20:07.720Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 37419
File: packages/i18n/src/locales/en.i18n.json:918-921
Timestamp: 2025-11-19T18:20:07.720Z
Learning: Repo: RocketChat/Rocket.Chat — i18n/formatting
Learning: This repository uses a custom message formatting parser in UI blocks/messages; do not assume standard Markdown rules. For keys like Call_ended_bold, Call_not_answered_bold, Call_failed_bold, and Call_transferred_bold in packages/i18n/src/locales/en.i18n.json, retain the existing single-asterisk emphasis unless maintainers request otherwise.

Applied to files:

  • apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js
🧬 Code graph analysis (2)
packages/livechat/src/widget.ts (2)
apps/meteor/client/lib/cachedStores/CachedStore.ts (1)
  • eventName (75-80)
apps/meteor/server/lib/callbacks.ts (1)
  • callbacks (218-226)
apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js (2)
packages/livechat/src/widget.ts (1)
  • ready (493-496)
packages/core-services/src/index.ts (1)
  • api (55-55)
⏰ 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: 📦 Build Packages
🔇 Additional comments (12)
apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js (6)

485-488: LGTM!

Clean initialization of module-scoped tracking variables with null defaults, consistent with the guard checks in the new helper functions.


520-551: LGTM!

The three detach/stop helpers follow a consistent guard → cleanup → reset pattern, and detachMediaQueryListener correctly handles both modern and legacy MediaQueryList APIs.


553-574: LGTM!

The teardown sequence correctly orchestrates cleanup: stops intervals, detaches all listeners, conditionally restores body styles/scroll when the widget was open on a small screen, removes the DOM node, and resets internal state.


734-748: LGTM!

The guard at the top of trackNavigation prevents duplicate intervals from accumulating across re-initializations.


756-758: Clean re-initialization guard.

Calling teardownWidget() before re-creating the widget in init ensures no duplicate listeners or orphaned DOM nodes survive a re-initialization cycle.


783-813: LGTM!

Storing the message listener and media-query listener as named references enables reliable teardown. The cross-browser fallback for addListener / addEventListener mirrors the detach logic correctly.

packages/livechat/src/widget.ts (6)

83-87: LGTM!

Well-typed module-scoped state variables for tracking callback handlers, navigation interval, message listener attachment, and media query resources.


89-116: Good fix for callback identity tracking.

Storing handlers in a Map<string, Set<...>> and using those exact references in clearAllCallbacks resolves the handler identity mismatch that previously prevented proper unregistration.


230-241: LGTM!

Media query listener setup is properly stored in module scope for later teardown, with cross-browser compatibility for both addEventListener and the deprecated addListener.


704-720: LGTM!

Using a boolean flag is appropriate here since onNewMessage is a stable module-level reference. The guards prevent duplicate attachment and safe no-op detachment.


762-778: LGTM!

Guard prevents duplicate interval creation, consistent with the JS counterpart.


786-788: LGTM!

Pre-initialization teardown ensures a clean slate for re-initialization, preventing listener and interval accumulation in SPA scenarios.

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

Inline comments:
In `@packages/livechat/src/widget.ts`:
- Around line 746-760: The teardownWidget function removes the mobile
full-screen class but doesn't restore the saved scroll position; update
teardownWidget to set document.documentElement.scrollTop (or window.scrollTo)
back to the previously saved scrollPosition and restore any body inline styles
you saved when opening the widget (e.g., document.body.style.position/overflow)
before clearing widget/iframe and resetting ready/hookQueue so mobile scroll is
returned to its prior state; locate teardownWidget and the saved scrollPosition
variable to add these restores right after the class removal and before nulling
widget/iframe.

@codecov
Copy link

codecov bot commented Feb 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.48%. Comparing base (508b4a1) to head (a2eb3ac).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #38745      +/-   ##
===========================================
- Coverage    70.51%   70.48%   -0.03%     
===========================================
  Files         3176     3176              
  Lines       111139   111139              
  Branches     20050    20033      -17     
===========================================
- Hits         78367    78338      -29     
- Misses       30721    30758      +37     
+ Partials      2051     2043       -8     
Flag Coverage Δ
e2e 60.37% <ø> (-0.11%) ⬇️
e2e-api 47.78% <ø> (+0.01%) ⬆️
unit 71.49% <ø> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

🧹 Nitpick comments (2)
apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js (2)

504-510: Legacy registerCallback doesn't track handler references for unregistration.

Unlike widget.ts which uses a callbackHandlers map to store handler references for proper cleanup in clearAllCallbacks, this legacy JS version registers callbacks on the emitter without storing references. Since the legacy JS doesn't expose clearAllCallbacks in its public API and the scope of this file is more limited, this is likely acceptable — but worth noting for parity if callback cleanup is ever needed here.

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

In `@apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js` around
lines 504 - 510, The registerCallback function registers event handlers but
doesn't retain references for later unregistration; update registerCallback to
mirror widget.ts by creating/using a callbackHandlers map keyed by eventName and
storing the returned handler reference when calling callbacks.on(eventName, fn),
so clearAllCallbacks (or future unregistration) can iterate callbackHandlers and
call callbacks.off/unregister using those stored references; touch the
registerCallback function, validCallbacks usage, the callbacks.on call, and
ensure any clearAllCallbacks logic operates against the new callbackHandlers
map.

553-578: teardownWidget correctly restores body styles and scroll on mobile.

The guarded restoration of bodyStyle and scrollPosition (Lines 559–563) only fires when the widget is open on a small screen, which is the correct behavior. DOM removal and state reset follow in the right order.

Minor nit: the if (currentPage) guard on Line 574 is always truthy since currentPage is a module-scoped object literal (Line 734), but it's harmless.

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

In `@apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js` around
lines 553 - 578, TeardownWidget contains an unnecessary truthiness guard around
currentPage (the module-scoped object literal currentPage is always truthy), so
remove the redundant if (currentPage) check in teardownWidget and directly null
out currentPage.href and currentPage.title; update references to currentPage
inside teardownWidget (function teardownWidget) to simply set currentPage.href =
null and currentPage.title = null to make intent clear and avoid a misleading
conditional.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14bc9fc and a2eb3ac.

📒 Files selected for processing (2)
  • apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js
  • packages/livechat/src/widget.ts
🧰 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:

  • packages/livechat/src/widget.ts
  • apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js
🧬 Code graph analysis (1)
apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js (2)
packages/livechat/src/widget.ts (1)
  • ready (493-496)
packages/core-services/src/index.ts (1)
  • api (55-55)
⏰ 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). (3)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: CodeQL-Build
🔇 Additional comments (8)
packages/livechat/src/widget.ts (5)

83-87: Good addition of lifecycle tracking state.

The module-scoped variables for tracking navigation interval, message listener attachment, media query list/listener provide the foundation for proper cleanup. Implementation looks correct.


704-720: Guard against duplicate message listeners is correct.

The messageListenerAttached boolean flag prevents stacking listeners across re-initializations. The detachMessageListener / attachMessageListener pair is symmetric and clean.


746-767: Scroll restoration on mobile teardown now addressed — verify document.body.classList cleanup is sufficient.

Lines 751–754 restore scroll position when the widget is open on a small screen, addressing the previously flagged concern. The typeof scrollPosition === 'number' guard correctly handles the case where scrollPosition was never set.

Note that unlike the legacy JS version (which restores document.body.style.cssText), this version only removes the CSS class (Line 755). This is consistent with how the TS widget manages mobile styles (via rc-livechat-mobile-full-screen class rather than inline styles), so this is correct.


769-785: Navigation interval guard prevents duplicate intervals.

The navigationIntervalId !== null check at the top of trackNavigation correctly prevents multiple concurrent intervals from being created on re-initialization.


787-801: Re-initialization tears down existing widget first — good.

Calling teardownWidget() before creating a new widget prevents duplicate DOM elements, listeners, and intervals. This directly addresses the SPA re-mount concern from the issue.

apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js (3)

485-488: Lifecycle state variables correctly mirror the TypeScript implementation.

The four new state variables (navigationInterval, messageListener, mediaQueryList, mediaQueryListener) enable proper teardown by retaining references to active listeners/timers.


787-795: Storing messageListener reference enables proper cleanup — good fix.

Previously the anonymous function couldn't be removed by removeEventListener. Assigning it to the module-scoped messageListener variable and passing that to both addEventListener and removeEventListener (in detachMessageListener) is the correct approach.


738-753: Navigation guard and init teardown are consistent with the TS implementation.

trackNavigation now correctly prevents duplicate intervals, and init tears down any existing widget before re-initializing. Both patterns match widget.ts.

Also applies to: 760-762

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

Inline comments:
In `@packages/livechat/src/widget.ts`:
- Around line 89-99: teardownWidget currently doesn't clear callback handlers
registered via registerCallback, so callbacks in callbackHandlers persist across
widget teardown/re-init; decide desired behavior and implement accordingly: if
you want full cleanup call clearAllCallbacks() inside teardownWidget (or invoke
callbackHandlers.clear() there) so registered handlers are removed, otherwise
add a comment in teardownWidget explaining that retaining callbacks is
intentional to preserve user handlers across re-initialization and document that
behavior; reference registerCallback, teardownWidget, clearAllCallbacks, and
callbackHandlers when making the change.

---

Nitpick comments:
In `@apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js`:
- Around line 504-510: The registerCallback function registers event handlers
but doesn't retain references for later unregistration; update registerCallback
to mirror widget.ts by creating/using a callbackHandlers map keyed by eventName
and storing the returned handler reference when calling callbacks.on(eventName,
fn), so clearAllCallbacks (or future unregistration) can iterate
callbackHandlers and call callbacks.off/unregister using those stored
references; touch the registerCallback function, validCallbacks usage, the
callbacks.on call, and ensure any clearAllCallbacks logic operates against the
new callbackHandlers map.
- Around line 553-578: TeardownWidget contains an unnecessary truthiness guard
around currentPage (the module-scoped object literal currentPage is always
truthy), so remove the redundant if (currentPage) check in teardownWidget and
directly null out currentPage.href and currentPage.title; update references to
currentPage inside teardownWidget (function teardownWidget) to simply set
currentPage.href = null and currentPage.title = null to make intent clear and
avoid a misleading conditional.

Comment on lines +89 to 99
function registerCallback(eventName: string, fn: (...args: unknown[]) => unknown) {
if (VALID_CALLBACKS.indexOf(eventName) === -1) {
return false;
}

const handlers = callbackHandlers.get(eventName) ?? new Set();
handlers.add(fn);
callbackHandlers.set(eventName, handlers);

return callbacks.on(eventName, fn);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'clearAllCallbacks|teardownWidget|function init' packages/livechat/src/widget.ts -A 5 -B 2

Repository: RocketChat/Rocket.Chat

Length of output: 1134


🏁 Script executed:

sed -n '746,800p' packages/livechat/src/widget.ts

Repository: RocketChat/Rocket.Chat

Length of output: 1236


Confirm whether cleanup of registered callbacks on widget teardown is intentional.

teardownWidget() does not invoke clearAllCallbacks(). If the goal is full cleanup when the widget is removed or re-initialized, callbacks registered through registerCallback will persist. Either document this as intentional behavior (to preserve user callbacks across re-initialization) or call clearAllCallbacks() from teardownWidget().

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

In `@packages/livechat/src/widget.ts` around lines 89 - 99, teardownWidget
currently doesn't clear callback handlers registered via registerCallback, so
callbacks in callbackHandlers persist across widget teardown/re-init; decide
desired behavior and implement accordingly: if you want full cleanup call
clearAllCallbacks() inside teardownWidget (or invoke callbackHandlers.clear()
there) so registered handlers are removed, otherwise add a comment in
teardownWidget explaining that retaining callbacks is intentional to preserve
user handlers across re-initialization and document that behavior; reference
registerCallback, teardownWidget, clearAllCallbacks, and callbackHandlers when
making the change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Livechat widget leaks event listeners and intervals on teardown / re-initialization

1 participant

Comments