diff --git a/src/gui/src/UI/Dashboard/TabNotifications.js b/src/gui/src/UI/Dashboard/TabNotifications.js
index d00f6c17c..8ddc75795 100644
--- a/src/gui/src/UI/Dashboard/TabNotifications.js
+++ b/src/gui/src/UI/Dashboard/TabNotifications.js
@@ -85,11 +85,12 @@ const BROWSER_DETAIL_KEYS = {
off: 'push_browser_off',
};
-/** @param {BrowserPushState} state */
-export const browserCardHtml = (state) => {
+/** @param {BrowserPushState} state @param {{ busy?: boolean }} [opts] - `busy` while the client is enabling or disabling */
+export const browserCardHtml = (state, { busy = false } = {}) => {
+ const disabled = busy ? ' disabled' : '';
let action = '';
- if ( state === 'on' ) action = ``;
- if ( state === 'off' ) action = ``;
+ if ( state === 'on' ) action = ``;
+ if ( state === 'off' ) action = ``;
return card({
icon: ICONS.bell,
title: i18n('push_this_browser'),
@@ -200,12 +201,27 @@ const TabNotifications = {
// Only the latest status read is drawn, so a slow one can't undo a newer state.
let browserSeq = 0;
+ let browserBusy = false;
const refreshBrowser = async () => {
const seq = ++browserSeq;
const state = await browserPushState(getPushClient());
- if ( seq === browserSeq ) $section.find('.push-browser-card').replaceWith(browserCardHtml(state));
+ if ( seq === browserSeq ) {
+ $section.find('.push-browser-card').replaceWith(browserCardHtml(state, { busy: browserBusy }));
+ }
return state;
};
+ // One client call at a time: the card's button is out until it settles,
+ // and a status read begun before it no longer draws the card.
+ const whileBrowserBusy = async (call) => {
+ browserBusy = true;
+ browserSeq++;
+ $section.find('.push-browser-card button').prop('disabled', true);
+ try {
+ return await call();
+ } finally {
+ browserBusy = false;
+ }
+ };
const renderDevices = () => {
const now = Date.now();
@@ -275,11 +291,10 @@ const TabNotifications = {
$section.on('click', '.push-turn-on', async function () {
const client = getPushClient();
- if ( ! client ) return;
- $(this).prop('disabled', true);
+ if ( ! client || browserBusy ) return;
let deviceUid;
try {
- ({ deviceUid } = await client.enable());
+ ({ deviceUid } = await whileBrowserBusy(() => client.enable()));
} catch ( e ) {
console.warn('[push] enable failed', e);
if ( await refreshBrowser() !== 'blocked' ) showError('push_enable_failed', $el_window);
@@ -301,10 +316,9 @@ const TabNotifications = {
$section.on('click', '.push-turn-off', async function () {
const client = getPushClient();
- if ( ! client ) return;
- $(this).prop('disabled', true);
+ if ( ! client || browserBusy ) return;
try {
- await client.disable();
+ await whileBrowserBusy(() => client.disable());
} catch ( e ) {
console.warn('[push] disable failed', e);
showError('push_disable_failed', $el_window);
diff --git a/src/gui/src/UI/Dashboard/TabNotifications.test.js b/src/gui/src/UI/Dashboard/TabNotifications.test.js
index 8a52e9b76..5305a7573 100644
--- a/src/gui/src/UI/Dashboard/TabNotifications.test.js
+++ b/src/gui/src/UI/Dashboard/TabNotifications.test.js
@@ -241,6 +241,92 @@ describe('this browser', () => {
expect(browserCard().hasClass('push-browser-on')).toBe(true);
});
+ it('turns on once however often the button is pressed while enabling', async () => {
+ const client = fakeClient();
+ pushClient.registerPushClient(client);
+ let finishEnable;
+ client.enable.mockImplementation(() => new Promise(resolve => {
+ finishEnable = resolve;
+ }));
+ serve({
+ 'GET /push/devices': { devices: [] },
+ 'GET /push/apps': { apps: [] },
+ 'GET /push/preferences': { puter: true },
+ 'POST /push/devices/dev-new/test': () => reply(200, { sent: true }),
+ });
+ await open();
+
+ $('.push-turn-on').trigger('click');
+ await settle();
+ expect($('.push-turn-on').prop('disabled')).toBe(true);
+ // A redraw while enabling keeps the button out.
+ await tab.onActivate($('.win'));
+ await settle();
+ expect($('.push-turn-on').prop('disabled')).toBe(true);
+ $('.push-turn-on').trigger('click');
+ $('.push-turn-on').prop('disabled', false).trigger('click');
+ await settle();
+ expect(client.enable).toHaveBeenCalledTimes(1);
+
+ client.status.mockResolvedValue({ permission: 'granted', registered: true, deviceUid: 'dev-new' });
+ finishEnable({ deviceUid: 'dev-new' });
+ await settle();
+ expect(browserCard().hasClass('push-browser-on')).toBe(true);
+ expect($('.push-turn-off').prop('disabled')).toBe(false);
+ });
+
+ it('lets no status read begun before Turn on draw the card while enabling', async () => {
+ const client = fakeClient();
+ pushClient.registerPushClient(client);
+ await open();
+ let finishSlow;
+ client.status.mockImplementationOnce(() => new Promise(resolve => {
+ finishSlow = resolve;
+ }));
+ let finishEnable;
+ client.enable.mockImplementation(() => new Promise(resolve => {
+ finishEnable = resolve;
+ }));
+ tab.onActivate($('.win'));
+ await settle();
+
+ $('.push-turn-on').trigger('click');
+ await settle();
+ finishSlow({ permission: 'default', registered: false });
+ await settle();
+ expect($('.push-turn-on').prop('disabled')).toBe(true);
+
+ client.status.mockResolvedValue({ permission: 'granted', registered: true, deviceUid: 'dev-new' });
+ finishEnable({ deviceUid: 'dev-new' });
+ await settle();
+ expect(browserCard().hasClass('push-browser-on')).toBe(true);
+ });
+
+ it('turns off once however often the button is pressed while disabling', async () => {
+ const client = fakeClient({ permission: 'granted', registered: true, deviceUid: 'dev-1' });
+ pushClient.registerPushClient(client);
+ let finishDisable;
+ client.disable.mockImplementation(() => new Promise(resolve => {
+ finishDisable = resolve;
+ }));
+ await open();
+
+ $('.push-turn-off').trigger('click');
+ await settle();
+ await tab.onActivate($('.win'));
+ await settle();
+ expect($('.push-turn-off').prop('disabled')).toBe(true);
+ $('.push-turn-off').prop('disabled', false).trigger('click');
+ await settle();
+ expect(client.disable).toHaveBeenCalledTimes(1);
+
+ client.status.mockResolvedValue({ permission: 'granted', registered: false });
+ finishDisable();
+ await settle();
+ expect(browserCard().hasClass('push-browser-off')).toBe(true);
+ expect($('.push-turn-on').prop('disabled')).toBe(false);
+ });
+
it('turns off through the client', async () => {
const client = fakeClient({ permission: 'granted', registered: true, deviceUid: 'dev-1' });
pushClient.registerPushClient(client);