Skip to content

Commit 14bc9fc

Browse files
fix(livechat): clean up widget teardown lifecycle
1 parent 508b4a1 commit 14bc9fc

File tree

2 files changed

+171
-14
lines changed

2 files changed

+171
-14
lines changed

apps/meteor/packages/rocketchat-livechat/assets/rocket-livechat.js

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,10 @@
482482
var smallScreen = false;
483483
var bodyStyle;
484484
var scrollPosition;
485+
var navigationInterval = null;
486+
var messageListener = null;
487+
var mediaQueryList = null;
488+
var mediaQueryListener = null;
485489

486490
var widgetWidth = '320px';
487491
var widgetHeightOpened = '350px';
@@ -513,6 +517,62 @@
513517
}
514518
};
515519

520+
var detachMessageListener = function() {
521+
if (!messageListener) {
522+
return;
523+
}
524+
525+
w.removeEventListener('message', messageListener, false);
526+
messageListener = null;
527+
};
528+
529+
var stopNavigationTracking = function() {
530+
if (navigationInterval === null) {
531+
return;
532+
}
533+
534+
clearInterval(navigationInterval);
535+
navigationInterval = null;
536+
};
537+
538+
var detachMediaQueryListener = function() {
539+
if (!mediaQueryList || !mediaQueryListener) {
540+
return;
541+
}
542+
543+
if (typeof mediaQueryList.removeEventListener === 'function') {
544+
mediaQueryList.removeEventListener('change', mediaQueryListener);
545+
} else {
546+
mediaQueryList.removeListener(mediaQueryListener);
547+
}
548+
549+
mediaQueryList = null;
550+
mediaQueryListener = null;
551+
};
552+
553+
var teardownWidget = function() {
554+
stopNavigationTracking();
555+
detachMessageListener();
556+
detachMediaQueryListener();
557+
558+
var isWidgetOpened = widget && widget.dataset && widget.dataset.state === 'opened';
559+
if (smallScreen && isWidgetOpened && typeof bodyStyle === 'string') {
560+
document.body.style.cssText = bodyStyle;
561+
if (typeof scrollPosition === 'number') {
562+
document.body.scrollTop = scrollPosition;
563+
}
564+
}
565+
566+
if (widget && widget.parentNode) {
567+
widget.parentNode.removeChild(widget);
568+
}
569+
570+
widget = null;
571+
iframe = null;
572+
ready = false;
573+
hookQueue = [];
574+
};
575+
516576
// hooks
517577
var callHook = function(action, params) {
518578
if (!ready) {
@@ -617,7 +677,7 @@
617677
openWidget();
618678
},
619679
removeWidget: function() {
620-
document.getElementsByTagName('body')[0].removeChild(widget);
680+
teardownWidget();
621681
},
622682
callback: function(eventName, data) {
623683
emitCallback(eventName, data);
@@ -672,7 +732,11 @@
672732
title: null
673733
};
674734
var trackNavigation = function() {
675-
setInterval(function() {
735+
if (navigationInterval !== null) {
736+
return;
737+
}
738+
739+
navigationInterval = setInterval(function() {
676740
if (document.location.href !== currentPage.href) {
677741
pageVisited('url');
678742
currentPage.href = document.location.href;
@@ -689,6 +753,10 @@
689753
return;
690754
}
691755

756+
if (widget) {
757+
teardownWidget();
758+
}
759+
692760
config.url = url;
693761

694762
var chatWidget = document.createElement('div');
@@ -712,14 +780,15 @@
712780
widget = document.querySelector('.rocketchat-widget');
713781
iframe = document.getElementById('rocketchat-iframe');
714782

715-
w.addEventListener('message', function(msg) {
783+
messageListener = function(msg) {
716784
if (typeof msg.data === 'object' && msg.data.src !== undefined && msg.data.src === 'rocketchat') {
717785
if (api[msg.data.fn] !== undefined && typeof api[msg.data.fn] === 'function') {
718786
var args = [].concat(msg.data.args || []);
719787
api[msg.data.fn].apply(null, args);
720788
}
721789
}
722-
}, false);
790+
};
791+
w.addEventListener('message', messageListener, false);
723792

724793
var mediaqueryresponse = function(mql) {
725794
if (mql.matches) {
@@ -734,9 +803,14 @@
734803
}
735804
};
736805

737-
var mql = window.matchMedia('screen and (max-device-width: 480px)');
738-
mediaqueryresponse(mql);
739-
mql.addListener(mediaqueryresponse);
806+
mediaQueryList = window.matchMedia('screen and (max-device-width: 480px)');
807+
mediaQueryListener = mediaqueryresponse;
808+
mediaqueryresponse(mediaQueryList);
809+
if (typeof mediaQueryList.addEventListener === 'function') {
810+
mediaQueryList.addEventListener('change', mediaQueryListener);
811+
} else {
812+
mediaQueryList.addListener(mediaQueryListener);
813+
}
740814

741815
// track user navigation
742816
trackNavigation();

packages/livechat/src/widget.ts

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,21 @@ export const VALID_CALLBACKS = [
8080
const VALID_SYSTEM_MESSAGES = ['uj', 'ul', 'livechat-close', 'livechat-started', 'livechat_transfer_history'];
8181

8282
const callbacks = new Emitter();
83+
const callbackHandlers = new Map<string, Set<(...args: unknown[]) => unknown>>();
84+
let navigationIntervalId: ReturnType<typeof setInterval> | null = null;
85+
let messageListenerAttached = false;
86+
let mediaQueryList: MediaQueryList | null = null;
87+
let mediaQueryListener: ((event: MediaQueryList | MediaQueryListEvent) => void) | null = null;
8388

84-
function registerCallback(eventName: string, fn: () => unknown) {
89+
function registerCallback(eventName: string, fn: (...args: unknown[]) => unknown) {
8590
if (VALID_CALLBACKS.indexOf(eventName) === -1) {
8691
return false;
8792
}
8893

94+
const handlers = callbackHandlers.get(eventName) ?? new Set();
95+
handlers.add(fn);
96+
callbackHandlers.set(eventName, handlers);
97+
8998
return callbacks.on(eventName, fn);
9099
}
91100

@@ -98,9 +107,12 @@ function emitCallback(eventName: string, data?: unknown) {
98107
}
99108

100109
function clearAllCallbacks() {
101-
callbacks.events().forEach((callback) => {
102-
callbacks.off(callback, () => undefined);
110+
callbackHandlers.forEach((handlers, eventName) => {
111+
handlers.forEach((handler) => {
112+
callbacks.off(eventName, handler);
113+
});
103114
});
115+
callbackHandlers.clear();
104116
}
105117

106118
const formatMessage = (action: keyof HooksWidgetAPI, ...params: Parameters<HooksWidgetAPI[keyof HooksWidgetAPI]>) => ({
@@ -215,8 +227,17 @@ const createWidget = (url: string) => {
215227
callHook('setParentUrl', window.location.href);
216228
};
217229

218-
const mediaQueryList = window.matchMedia('screen and (max-device-width: 480px)');
219-
mediaQueryList.addListener(handleMediaQueryTest);
230+
mediaQueryList = window.matchMedia('screen and (max-device-width: 480px)');
231+
mediaQueryListener = (event) => {
232+
handleMediaQueryTest(event);
233+
};
234+
235+
if (typeof mediaQueryList.addEventListener === 'function') {
236+
mediaQueryList.addEventListener('change', mediaQueryListener);
237+
} else {
238+
mediaQueryList.addListener(mediaQueryListener);
239+
}
240+
220241
handleMediaQueryTest(mediaQueryList);
221242
};
222243

@@ -503,7 +524,7 @@ const api: InternalWidgetAPI = {
503524
},
504525

505526
removeWidget() {
506-
document.body.removeChild(widget as Node);
527+
teardownWidget();
507528
},
508529

509530
callback(eventName, data) {
@@ -680,12 +701,70 @@ function listenForMessageOnce<K extends keyof InternalWidgetAPI>(
680701
window.addEventListener('message', listener);
681702
}
682703

704+
const detachMessageListener = () => {
705+
if (!messageListenerAttached) {
706+
return;
707+
}
708+
709+
window.removeEventListener('message', onNewMessage, false);
710+
messageListenerAttached = false;
711+
};
712+
683713
const attachMessageListener = () => {
714+
if (messageListenerAttached) {
715+
return;
716+
}
717+
684718
window.addEventListener('message', onNewMessage, false);
719+
messageListenerAttached = true;
720+
};
721+
722+
const stopNavigationTracking = () => {
723+
if (navigationIntervalId === null) {
724+
return;
725+
}
726+
727+
clearInterval(navigationIntervalId);
728+
navigationIntervalId = null;
729+
};
730+
731+
const detachMediaQueryListener = () => {
732+
if (!mediaQueryList || !mediaQueryListener) {
733+
return;
734+
}
735+
736+
if (typeof mediaQueryList.removeEventListener === 'function') {
737+
mediaQueryList.removeEventListener('change', mediaQueryListener);
738+
} else {
739+
mediaQueryList.removeListener(mediaQueryListener);
740+
}
741+
742+
mediaQueryList = null;
743+
mediaQueryListener = null;
744+
};
745+
746+
const teardownWidget = () => {
747+
stopNavigationTracking();
748+
detachMessageListener();
749+
detachMediaQueryListener();
750+
document.body.classList.remove('rc-livechat-mobile-full-screen');
751+
752+
if (widget?.parentNode) {
753+
widget.parentNode.removeChild(widget);
754+
}
755+
756+
widget = null;
757+
iframe = null;
758+
ready = false;
759+
hookQueue = [];
685760
};
686761

687762
const trackNavigation = () => {
688-
setInterval(() => {
763+
if (navigationIntervalId !== null) {
764+
return;
765+
}
766+
767+
navigationIntervalId = setInterval(() => {
689768
if (document.location.href !== currentPage.href) {
690769
pageVisited('url');
691770
currentPage.href = document.location.href;
@@ -704,6 +783,10 @@ const init = (url: string) => {
704783
return;
705784
}
706785

786+
if (widget) {
787+
teardownWidget();
788+
}
789+
707790
config.url = trimmedUrl;
708791

709792
createWidget(trimmedUrl);

0 commit comments

Comments
 (0)