Skip to content

Commit 9d4c15d

Browse files
authored
Merge pull request #190 from GleapSDK/fix-225880-clear-identity-notification-cache
Clear cached notifications on clearIdentity before the UI mounts (#225880)
2 parents ece8c93 + e6db374 commit 9d4c15d

2 files changed

Lines changed: 81 additions & 7 deletions

File tree

‎src/GleapNotificationManager.js‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,12 @@ export default class GleapNotificationManager {
9191
try {
9292
const notificationsFromCache = loadFromGleapCache(this.unreadNotificationsKey);
9393
if (notificationsFromCache && notificationsFromCache.length > 0) {
94+
const gleapId = GleapSession.getInstance().session?.gleapId;
9495
let nots = notificationsFromCache.filter(
9596
(notification) =>
97+
// Only the contact that received it (unstamped pre-fix entries are dropped).
98+
gleapId &&
99+
notification?.cachedForGleapId === gleapId &&
96100
// Never resurface chatbar-routed notifications in the widget bubble.
97101
// (Pre-fix builds may have cached them before the routing guard existed.)
98102
notification?.data?.lastSource !== 'chatbar' &&
@@ -187,6 +191,9 @@ export default class GleapNotificationManager {
187191

188192
const notificationsForOutbound = this.notifications.find((e) => notification.outbound === e.outbound);
189193
if (!notificationsForOutbound) {
194+
// The cache key is shared by every contact on this browser; remember who
195+
// received it so a reload never shows it to another contact.
196+
notification.cachedForGleapId = GleapSession.getInstance().session?.gleapId;
190197
this.notifications.push(notification);
191198

192199
// Play sound only if it's a new one and its page rules allow the
@@ -645,17 +652,20 @@ export default class GleapNotificationManager {
645652
* @param {boolean} fromOtherTab - True when applying a clear broadcast from a sibling tab (prevents re-broadcast loops).
646653
*/
647654
clearAllNotifications(uiOnly = false, fromOtherTab = false) {
648-
if (!this.notificationContainer) {
649-
return;
650-
}
651-
655+
// Clear the state and cache even before the UI is mounted: a clearIdentity()
656+
// queued in the snippet runs before injectNotificationUI(), which would
657+
// otherwise replay the previous contact's notifications from the cache.
652658
if (!uiOnly) {
653659
this.notifications = [];
654660
// Reset currentNewsIndex whenever we clear them out
655661
this.currentNewsIndex = 0;
656662
saveToGleapCache(this.unreadNotificationsKey, this.notifications);
657663
}
658664

665+
if (!this.notificationContainer) {
666+
return;
667+
}
668+
659669
while (this.notificationContainer.firstChild) {
660670
this.notificationContainer.removeChild(this.notificationContainer.firstChild);
661671
}

‎src/GleapNotificationManager.test.js‎

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ describe('page rules — render-time evaluation (#141052)', () => {
253253
test('cached page-ruled notifications survive a reload on an excluded page but stay hidden', () => {
254254
setUrl(EXCLUDED_URL);
255255
const nm = withContainer(GleapNotificationManager.getInstance());
256-
const cached = pageRuledNotification();
256+
const cached = { ...pageRuledNotification(), cachedForGleapId: 'user-1' };
257257
loadFromGleapCache.mockReturnValue([cached]);
258258

259259
nm.reloadNotificationsFromCache();
@@ -319,7 +319,9 @@ describe('notification stack', () => {
319319
test('reloadNotificationsFromCache keeps the NEWEST 4 (the same end shift() trims from)', () => {
320320
const nm = withContainer(GleapNotificationManager.getInstance());
321321
jest.spyOn(nm, 'renderNotifications').mockImplementation(() => {});
322-
loadFromGleapCache.mockReturnValue([1, 2, 3, 4, 5, 6].map((i) => plainNotification(`n${i}`)));
322+
loadFromGleapCache.mockReturnValue(
323+
[1, 2, 3, 4, 5, 6].map((i) => ({ ...plainNotification(`n${i}`), cachedForGleapId: 'user-1' }))
324+
);
323325

324326
nm.reloadNotificationsFromCache();
325327

@@ -365,9 +367,71 @@ describe('clearAllNotifications — cross-tab broadcast gate', () => {
365367
expect(sendMessageMock).not.toHaveBeenCalled();
366368
});
367369

368-
test('no notification container => early return, no broadcast', () => {
370+
test('no notification container => no broadcast (the tab channel only starts with the UI)', () => {
369371
const nm = GleapNotificationManager.getInstance();
370372
nm.clearAllNotifications();
371373
expect(sendMessageMock).not.toHaveBeenCalled();
372374
});
373375
});
376+
377+
describe('notification cache vs identity (#225880)', () => {
378+
const plainNotification = (text, gleapId) => ({
379+
outbound: `outbound-${text}`,
380+
createdAt: new Date().toISOString(),
381+
data: { text },
382+
...(gleapId ? { cachedForGleapId: gleapId } : {}),
383+
});
384+
385+
beforeEach(() => {
386+
loadFromGleapCache.mockReset();
387+
saveToGleapCache.mockReset();
388+
});
389+
390+
test('clearAllNotifications(false) clears the persisted cache before the UI is mounted', () => {
391+
// A clearIdentity() queued in the snippet runs before injectNotificationUI().
392+
const nm = GleapNotificationManager.getInstance();
393+
nm.notifications = [plainNotification('previous user')];
394+
395+
nm.clearAllNotifications(false);
396+
397+
expect(nm.notifications).toEqual([]);
398+
expect(saveToGleapCache).toHaveBeenCalledWith(nm.unreadNotificationsKey, []);
399+
});
400+
401+
test('a UI-only clear before mount leaves the cache alone', () => {
402+
const nm = GleapNotificationManager.getInstance();
403+
const notification = plainNotification('keep');
404+
nm.notifications = [notification];
405+
406+
nm.clearAllNotifications(true);
407+
408+
expect(nm.notifications).toEqual([notification]);
409+
expect(saveToGleapCache).not.toHaveBeenCalled();
410+
});
411+
412+
test('showNotification stamps the receiving gleapId on the cached notification', () => {
413+
const nm = withContainer(GleapNotificationManager.getInstance());
414+
jest.spyOn(nm, 'renderNotifications').mockImplementation(() => {});
415+
416+
nm.showNotification(plainNotification('hello'));
417+
418+
expect(saveToGleapCache).toHaveBeenCalledWith(nm.unreadNotificationsKey, [
419+
expect.objectContaining({ cachedForGleapId: 'user-1' }),
420+
]);
421+
});
422+
423+
test('reloadNotificationsFromCache drops notifications cached for another (or no) gleapId', () => {
424+
const nm = withContainer(GleapNotificationManager.getInstance());
425+
jest.spyOn(nm, 'renderNotifications').mockImplementation(() => {});
426+
const own = plainNotification('own', 'user-1');
427+
loadFromGleapCache.mockReturnValue([
428+
plainNotification('previous user', 'user-0'),
429+
plainNotification('unstamped'),
430+
own,
431+
]);
432+
433+
nm.reloadNotificationsFromCache();
434+
435+
expect(nm.notifications).toEqual([own]);
436+
});
437+
});

0 commit comments

Comments
 (0)