Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/GleapNotificationManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,12 @@ export default class GleapNotificationManager {
try {
const notificationsFromCache = loadFromGleapCache(this.unreadNotificationsKey);
if (notificationsFromCache && notificationsFromCache.length > 0) {
const gleapId = GleapSession.getInstance().session?.gleapId;
let nots = notificationsFromCache.filter(
(notification) =>
// Only the contact that received it (unstamped pre-fix entries are dropped).
gleapId &&
notification?.cachedForGleapId === gleapId &&
// Never resurface chatbar-routed notifications in the widget bubble.
// (Pre-fix builds may have cached them before the routing guard existed.)
notification?.data?.lastSource !== 'chatbar' &&
Expand Down Expand Up @@ -187,6 +191,9 @@ export default class GleapNotificationManager {

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

// Play sound only if it's a new one and its page rules allow the
Expand Down Expand Up @@ -645,17 +652,20 @@ export default class GleapNotificationManager {
* @param {boolean} fromOtherTab - True when applying a clear broadcast from a sibling tab (prevents re-broadcast loops).
*/
clearAllNotifications(uiOnly = false, fromOtherTab = false) {
if (!this.notificationContainer) {
return;
}

// Clear the state and cache even before the UI is mounted: a clearIdentity()
// queued in the snippet runs before injectNotificationUI(), which would
// otherwise replay the previous contact's notifications from the cache.
if (!uiOnly) {
this.notifications = [];
// Reset currentNewsIndex whenever we clear them out
this.currentNewsIndex = 0;
saveToGleapCache(this.unreadNotificationsKey, this.notifications);
}

if (!this.notificationContainer) {
return;
}

while (this.notificationContainer.firstChild) {
this.notificationContainer.removeChild(this.notificationContainer.firstChild);
}
Expand Down
70 changes: 67 additions & 3 deletions src/GleapNotificationManager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ describe('page rules — render-time evaluation (#141052)', () => {
test('cached page-ruled notifications survive a reload on an excluded page but stay hidden', () => {
setUrl(EXCLUDED_URL);
const nm = withContainer(GleapNotificationManager.getInstance());
const cached = pageRuledNotification();
const cached = { ...pageRuledNotification(), cachedForGleapId: 'user-1' };
loadFromGleapCache.mockReturnValue([cached]);

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

nm.reloadNotificationsFromCache();

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

test('no notification container => early return, no broadcast', () => {
test('no notification container => no broadcast (the tab channel only starts with the UI)', () => {
const nm = GleapNotificationManager.getInstance();
nm.clearAllNotifications();
expect(sendMessageMock).not.toHaveBeenCalled();
});
});

describe('notification cache vs identity (#225880)', () => {
const plainNotification = (text, gleapId) => ({
outbound: `outbound-${text}`,
createdAt: new Date().toISOString(),
data: { text },
...(gleapId ? { cachedForGleapId: gleapId } : {}),
});

beforeEach(() => {
loadFromGleapCache.mockReset();
saveToGleapCache.mockReset();
});

test('clearAllNotifications(false) clears the persisted cache before the UI is mounted', () => {
// A clearIdentity() queued in the snippet runs before injectNotificationUI().
const nm = GleapNotificationManager.getInstance();
nm.notifications = [plainNotification('previous user')];

nm.clearAllNotifications(false);

expect(nm.notifications).toEqual([]);
expect(saveToGleapCache).toHaveBeenCalledWith(nm.unreadNotificationsKey, []);
});

test('a UI-only clear before mount leaves the cache alone', () => {
const nm = GleapNotificationManager.getInstance();
const notification = plainNotification('keep');
nm.notifications = [notification];

nm.clearAllNotifications(true);

expect(nm.notifications).toEqual([notification]);
expect(saveToGleapCache).not.toHaveBeenCalled();
});

test('showNotification stamps the receiving gleapId on the cached notification', () => {
const nm = withContainer(GleapNotificationManager.getInstance());
jest.spyOn(nm, 'renderNotifications').mockImplementation(() => {});

nm.showNotification(plainNotification('hello'));

expect(saveToGleapCache).toHaveBeenCalledWith(nm.unreadNotificationsKey, [
expect.objectContaining({ cachedForGleapId: 'user-1' }),
]);
});

test('reloadNotificationsFromCache drops notifications cached for another (or no) gleapId', () => {
const nm = withContainer(GleapNotificationManager.getInstance());
jest.spyOn(nm, 'renderNotifications').mockImplementation(() => {});
const own = plainNotification('own', 'user-1');
loadFromGleapCache.mockReturnValue([
plainNotification('previous user', 'user-0'),
plainNotification('unstamped'),
own,
]);

nm.reloadNotificationsFromCache();

expect(nm.notifications).toEqual([own]);
});
});
Loading