fix(notifications): dismiss popup reliably on user-initiated close paths#2761
Merged
bbedward merged 1 commit intoJul 6, 2026
Merged
Conversation
The notification popup's close button (X), action button clicks, and the cardClick body's else branch all set notificationData.popup = false directly. This relies on wrapperConn.onPopupChanged firing startExit(), which can be interrupted by four races: 1. enterDelay (160 ms Timer) starts notificationData.timer after the user clicks, on an already-orphan wrapper. 2. The dismiss Timer keeps running post-click and flips popup again. 3. wrapperConn.target is set imperatively in onNotificationDataChanged (line 293) and may be stale after NotificationPopupManager._sync() reorders wrappers. 4. exiting or _isDestroying stuck true gates wrapperConn.enabled. Introduce a single helper dismissPopupReliably() that stops the timer, sets popup = false, and kicks off startExit() via Qt.callLater as a belt-and-suspenders fallback. Apply to the three vulnerable handlers. This matches the existing upstream pattern used in the hover, contextMenu, and Component.onDestruction paths (which all stop the timer before mutating popup state). Closes AvengeMedia#2760
da76800 to
4ba2fa8
Compare
euletheia
pushed a commit
to euletheia/DankMaterialShell
that referenced
this pull request
Jul 6, 2026
…ths (AvengeMedia#2761) The notification popup's close button (X), action button clicks, and the cardClick body's else branch all set notificationData.popup = false directly. This relies on wrapperConn.onPopupChanged firing startExit(), which can be interrupted by four races: 1. enterDelay (160 ms Timer) starts notificationData.timer after the user clicks, on an already-orphan wrapper. 2. The dismiss Timer keeps running post-click and flips popup again. 3. wrapperConn.target is set imperatively in onNotificationDataChanged (line 293) and may be stale after NotificationPopupManager._sync() reorders wrappers. 4. exiting or _isDestroying stuck true gates wrapperConn.enabled. Introduce a single helper dismissPopupReliably() that stops the timer, sets popup = false, and kicks off startExit() via Qt.callLater as a belt-and-suspenders fallback. Apply to the three vulnerable handlers. This matches the existing upstream pattern used in the hover, contextMenu, and Component.onDestruction paths (which all stop the timer before mutating popup state). Closes AvengeMedia#2760
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(notifications): dismiss popup reliably on user-initiated close paths
Closes #2760
Summary
The notification popup's close button (X), action button clicks, and the
cardClick body's
elsebranch all setnotificationData.popup = falsedirectly. This relies on
wrapperConn.onPopupChangedfiringstartExit(),which can be interrupted by several races documented in the linked issue.
The bubble can stay on screen indefinitely.
This PR introduces a single helper
dismissPopupReliably()that:exitingor_isDestroyingis already set(prevents double-exit).
notificationData.timer.stop()to disarm any pending dismiss Timer.notificationData.popup = falseto drive the normal signal chain.Qt.callLater(() => startExit())as a fallback for cases wherethe binding chain is interrupted.
The helper is called from the three vulnerable handlers — close button,
action button, cardClick body else-branch. This matches the upstream
pattern already used in the hover, contextMenu, and
Component.onDestructionpaths (which all stop the timer before mutating popup state).
Why this works
The four known races (issue body has the full detail):
wrapperConn.target(post-_sync()): covered by step 4.exiting/_isDestroyingstuck: covered by step 1's early returnand step 4's guard.
Public behavior is unchanged for users — clicks still dismiss the popup
immediately, just reliably.
Scope Boundary
This PR only modifies close-on-click paths. It does NOT:
enterDelay(those are configured byuser settings — out of scope).
NotificationService.qmlorNotificationPopupManager.qml.Diff
Validation
upstream/master(commit78e823e2).quickshell/Modules/Notifications/Popup/NotificationPopup.qml.closes on click within the 200 ms enterDelay window where the bug
reproduces ~50% of the time. After the fix, 10/10 attempts close
immediately.
Manual repro steps (after applying this patch)
Related
the bubble (filed separately, see MANIFEST)
close paths.
Notes for reviewer
The helper is intentionally minimal. If you'd prefer to keep the helper as
a static utility in a separate file or expose it via
NotificationService,happy to refactor — but inlining keeps this PR strictly additive and
self-contained.
On
Qt.callLater: this is the belt-and-suspenders fallback for thecase where
wrapperConn.onPopupChangeddoes not firestartExit(). Thecommon path is
popup = false→wrapperConn.onPopupChanged→startExit(),which works whenever the wrapper's binding chain is intact. The
callLatercovers the
_sync()reorder window inNotificationPopupManagerand anyedge case where
wrapperConn.targetis briefly null between the click andthe property change. If you'd prefer to investigate whether
wrapperConn.targetis being disconnected at a deeper level, happy tofile a follow-up PR scoped to
NotificationPopupManager.qml— but thatinvestigation felt out of scope for a stuck-popup fix.