From 1c2d33f149c07e1e17a2b9d9cd5e06253fa3580c Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Tue, 11 Aug 2026 18:26:47 -0700 Subject: [PATCH] fix: gate feedback dialog Cancel/X on an in-flight submit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Escape and backdrop clicks were already ignored while the POST was pending, but the X, Close, and Cancel buttons weren't — clicking one mid-send settled the promise false and tore down the overlay while the submission still landed server-side: the developer got the email, the app was told sent=false, and a user who resubmitted 'the failed one' sent a duplicate and burned a daily-cap slot. Apply the same !sending gate to the buttons and disable them visually while the send is in flight. --- src/gui/src/UI/UIWindowAppFeedback.js | 10 +++++++++- src/gui/src/css/dashboard.css | 10 ++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/gui/src/UI/UIWindowAppFeedback.js b/src/gui/src/UI/UIWindowAppFeedback.js index 66f789213..8fba932a4 100644 --- a/src/gui/src/UI/UIWindowAppFeedback.js +++ b/src/gui/src/UI/UIWindowAppFeedback.js @@ -142,7 +142,13 @@ async function UIWindowAppFeedback (options) { $overlay.on('mousedown', (e) => { if ( e.target === $overlay.get(0) && ! sending ) close(); }); - $overlay.find('.app-feedback-x, .app-feedback-close-btn, .app-feedback-cancel-btn').on('click', close); + // Same in-flight gate as Escape/backdrop: closing while the POST is + // pending would settle false for a submission that still lands + // server-side — the developer gets the email while the app is told + // nothing was sent. + $overlay.find('.app-feedback-x, .app-feedback-close-btn, .app-feedback-cancel-btn').on('click', () => { + if ( ! sending ) close(); + }); $overlay.find('.app-feedback-message').on('input', function () { $overlay.find('.app-feedback-counter').text(`${this.value.length} / ${MESSAGE_MAX_LENGTH}`); @@ -155,6 +161,7 @@ async function UIWindowAppFeedback (options) { if ( ! message || sending ) return; sending = true; $btn.prop('disabled', true); + $overlay.find('.app-feedback-x, .app-feedback-cancel-btn').prop('disabled', true); $overlay.find('.app-feedback-error').hide(); try { const res = await fetch(`${window.api_origin}/app-feedback`, { @@ -193,6 +200,7 @@ async function UIWindowAppFeedback (options) { $overlay.find('.app-feedback-error').text(i18n('app_feedback_error')).show(); } finally { sending = false; + $overlay.find('.app-feedback-x, .app-feedback-cancel-btn').prop('disabled', false); $overlay.find('.app-feedback-send-btn').prop('disabled', settled || String($overlay.find('.app-feedback-message').val() || '').trim() === ''); } diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index 20c0669cb..618704a1f 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -1619,7 +1619,7 @@ input.myapps-group-name:focus { height: 16px; } @media (hover: hover) { - .app-feedback-x:hover { + .app-feedback-x:hover:not(:disabled) { background: var(--afb-hover); color: var(--afb-fg); } @@ -1727,7 +1727,7 @@ input.myapps-group-name:focus { transition: background-color 0.12s ease, opacity 0.12s ease, filter 0.12s ease; } @media (hover: hover) { - .app-feedback-btn:hover { + .app-feedback-btn:hover:not(:disabled) { background: var(--afb-hover); } } @@ -1747,6 +1747,12 @@ input.myapps-group-name:focus { cursor: default; filter: none; } +/* Cancel/X are disabled while a submit is in flight. */ +.app-feedback-btn:disabled, +.app-feedback-x:disabled { + opacity: 0.5; + cursor: default; +} .app-feedback-success { text-align: center;