From 7aa4d91ae950b5695aa82b18e1d16d9da8a58b9d Mon Sep 17 00:00:00 2001 From: Juan Castro Date: Tue, 27 Oct 2026 14:17:57 -0400 Subject: [PATCH] fix(gui): a tap delivered twice routes once The page remembers the notifications it handed over in the last ten seconds and drops a repeat, whichever way it arrives. --- src/gui/src/helpers/notificationTap.js | 15 ++++++++++++++- src/gui/src/helpers/notificationTap.test.js | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/gui/src/helpers/notificationTap.js b/src/gui/src/helpers/notificationTap.js index 36dbbb866..3c9420f3c 100644 --- a/src/gui/src/helpers/notificationTap.js +++ b/src/gui/src/helpers/notificationTap.js @@ -27,6 +27,8 @@ export const PUSH_TAP_PARAM = 'puter.notification'; const UID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const UID_MAX = 64; +// A tap for a uid acted on this recently is the same tap again. +const TAP_REPEAT_MS = 10_000; const nonEmpty = (value) => (typeof value === 'string' && value !== '' ? value : undefined); @@ -200,6 +202,8 @@ export const storedTapHandler = ({ fetchRow, surface, found, missing }) => async let pushTapHandler = null; /** @type {PushTap[]} */ const pendingPushTaps = []; +/** @type {Map} uid → when it was last handed over */ +const recentTaps = new Map(); /** @param {(tap: PushTap) => unknown} handler */ export const setPushTapHandler = (handler) => { @@ -207,9 +211,18 @@ export const setPushTapHandler = (handler) => { for ( const tap of pendingPushTaps.splice(0) ) void handler(tap); }; +const seenLately = (uid, now) => { + for ( const [seen, at] of recentTaps ) { + if ( now - at >= TAP_REPEAT_MS ) recentTaps.delete(seen); + } + if ( recentTaps.has(uid) ) return true; + recentTaps.set(uid, now); + return false; +}; + /** @param {PushTap|null} tap */ const deliverPushTap = (tap) => { - if ( ! tap ) return; + if ( ! tap || seenLately(tap.uid, Date.now()) ) return; if ( pushTapHandler ) void pushTapHandler(tap); else pendingPushTaps.push(tap); }; diff --git a/src/gui/src/helpers/notificationTap.test.js b/src/gui/src/helpers/notificationTap.test.js index f84063c51..74a9488d6 100644 --- a/src/gui/src/helpers/notificationTap.test.js +++ b/src/gui/src/helpers/notificationTap.test.js @@ -282,6 +282,10 @@ describe('storedTapHandler', () => { }); describe('push tap dispatch', () => { + afterEach(() => { + vi.useRealTimers(); + }); + it('holds taps until a handler is set, then hands them over in order', async () => { const { dispatchPushTap, setPushTapHandler } = await import('./notificationTap.js'); dispatchPushTap({ uid: UID }); @@ -305,4 +309,21 @@ describe('push tap dispatch', () => { expect(fetchRow).toHaveBeenCalledWith(OTHER_UID); expect(openApp).not.toHaveBeenCalled(); }); + + it('hands the same notification over once within ten seconds', async () => { + vi.useFakeTimers(); + const { dispatchPushTap, setPushTapHandler } = await import('./notificationTap.js'); + const handler = vi.fn(); + setPushTapHandler(handler); + dispatchPushTap({ uid: UID }); + dispatchPushTap({ uid: UID }); + dispatchPushTap({ uid: OTHER_UID }); + expect(handler.mock.calls).toEqual([[{ uid: UID }], [{ uid: OTHER_UID }]]); + vi.advanceTimersByTime(9_999); + dispatchPushTap({ uid: UID }); + expect(handler).toHaveBeenCalledTimes(2); + vi.advanceTimersByTime(1); + dispatchPushTap({ uid: UID }); + expect(handler).toHaveBeenCalledTimes(3); + }); });