From 1c91eaa7fb93536a1e3e2695d856721d403171f6 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 5 May 2026 17:26:27 +0200 Subject: [PATCH 1/3] animationUtils: Skip no-op wiggle to avoid cancelling pending transitions When wiggle() is called with duration 0, the async easeAsync calls yield between microtasks, leaving pending operations that remove transitions on the same property. This becomes a problem when two setMessage() calls happen in the same call stack (e.g. via _queuePriorityMessage): the first fires a no-op wiggle(dur:0) and the second starts the real wiggle(dur:65). When microtasks drain, the first wiggle's pending easeAsync calls overwrite the second wiggle's active transition, effectively cancelling the real animation. Return early when duration is 0, since the animation is a no-op anyway. Fixes: fdadbb7a5a2 ("animationUtils: Use easeAsync() for wiggle") Closes: https://gitlab.gnome.org/GNOME/gnome-shell/-/work_items/9207 Part-of: --- js/misc/animationUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/misc/animationUtils.js b/js/misc/animationUtils.js index cf6889609..9d065cf78 100644 --- a/js/misc/animationUtils.js +++ b/js/misc/animationUtils.js @@ -84,7 +84,7 @@ export function ensureActorVisibleInScrollView(scrollView, actor) { * @param {number} params.wiggleCount - the number of times to wiggle the actor */ export function wiggle(actor, params) { - if (!St.Settings.get().enable_animations) + if (!St.Settings.get().enable_animations || params?.duration === 0) return; params = Params.parse(params, { -- 2.55.0 From b5a37cbdd89eeed44966455ba22b0f10a3f25091 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 5 May 2026 21:32:06 +0200 Subject: [PATCH 2/3] authPrompt: Call wiggle directly in _onShowMessage Part-of: --- js/gdm/authPrompt.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js index 3b4a2f798..f05856416 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -394,21 +394,20 @@ export const AuthPrompt = GObject.registerClass({ } _onShowMessage(_userVerifier, serviceName, message, type) { - let wiggleParameters = {duration: 0}; + this.setMessage(message, type); + this.emit('prompted'); if (type === GdmUtil.MessageType.ERROR && this._userVerifier.serviceIsFingerprint(serviceName)) { // TODO: Use Await for wiggle to be over before unfreezing the user verifier queue - wiggleParameters = { + const wiggleParameters = { duration: 65, wiggleCount: 3, }; this._userVerifier.increaseCurrentMessageTimeout( wiggleParameters.duration * (wiggleParameters.wiggleCount + 2)); + wiggle(this._message, wiggleParameters); } - - this.setMessage(message, type, wiggleParameters); - this.emit('prompted'); } _onVerificationFailed(userVerifier, serviceName, canRetry) { @@ -587,7 +586,7 @@ export const AuthPrompt = GObject.registerClass({ }); } - setMessage(message, type, wiggleParameters = {duration: 0}) { + setMessage(message, type) { if (type === GdmUtil.MessageType.ERROR) this._message.add_style_class_name('login-dialog-message-warning'); else @@ -607,8 +606,6 @@ export const AuthPrompt = GObject.registerClass({ } else { this._message.opacity = 0; } - - wiggle(this._message, wiggleParameters); } updateSensitivity(sensitive) { -- 2.55.0 From 1e3f937a6ccfc8ea79c80d78df121102cffbdeff Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 5 May 2026 17:28:18 +0200 Subject: [PATCH 3/3] authPrompt: Await wiggle via show-message promise resolver Pass promise resolver through the show-message signal so the message queue waits for the wiggle animation to finish before scheduling the next message timeout. This replaces the previous approach of manually increasing the timeout duration to account for the wiggle. Part-of: --- js/gdm/authPrompt.js | 21 +++++++++------------ js/gdm/util.js | 27 +++++++++++++++++---------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js index f05856416..b1e8202bd 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -393,21 +393,18 @@ export const AuthPrompt = GObject.registerClass({ this.reset(); } - _onShowMessage(_userVerifier, serviceName, message, type) { + _onShowMessage(_userVerifier, serviceName, message, type, showMessageResolver) { this.setMessage(message, type); this.emit('prompted'); - if (type === GdmUtil.MessageType.ERROR && - this._userVerifier.serviceIsFingerprint(serviceName)) { - // TODO: Use Await for wiggle to be over before unfreezing the user verifier queue - const wiggleParameters = { - duration: 65, - wiggleCount: 3, - }; - this._userVerifier.increaseCurrentMessageTimeout( - wiggleParameters.duration * (wiggleParameters.wiggleCount + 2)); - wiggle(this._message, wiggleParameters); - } + const shouldWiggle = type === GdmUtil.MessageType.ERROR && + this._userVerifier.serviceIsFingerprint(serviceName); + + const wigglePromise = shouldWiggle + ? wiggle(this._message, {duration: 65, wiggleCount: 3}) + : Promise.resolve(); + + showMessageResolver?.(wigglePromise); } _onVerificationFailed(userVerifier, serviceName, canRetry) { diff --git a/js/gdm/util.js b/js/gdm/util.js index 4b0f763f0..e529f84fa 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -262,11 +262,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { this.emit('no-more-messages'); } - increaseCurrentMessageTimeout(interval) { - if (!this._messageQueueTimeoutId && interval > 0) - this._currentMessageExtraInterval = interval; - } - _serviceHasPendingMessages(serviceName) { return this._messageQueue.some(m => m.serviceName === serviceName); } @@ -279,17 +274,23 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._queuePriorityMessage(serviceName, null, messageType); } - _queueMessageTimeout() { - if (this._messageQueueTimeoutId !== 0) + async _queueMessageTimeout() { + if (this._messageQueueTimeoutId !== 0 || this._showMessageResolver) return; const message = this.currentMessage; + const {promise, resolve} = Promise.withResolvers(); + this._showMessageResolver = resolve; - delete this._currentMessageExtraInterval; - this.emit('show-message', message.serviceName, message.text, message.type); + this.emit('show-message', message.serviceName, message.text, message.type, this._showMessageResolver); + + await promise.catch(logError); + if (!this._showMessageResolver) + return; + this._showMessageResolver = null; this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, - message.interval + (this._currentMessageExtraInterval | 0), () => { + message.interval, () => { this._messageQueueTimeoutId = 0; if (this._messageQueue.length > 1) { @@ -332,6 +333,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { GLib.source_remove(this._messageQueueTimeoutId); this._messageQueueTimeoutId = 0; } + + if (this._showMessageResolver) { + this._showMessageResolver(); + this._showMessageResolver = null; + } + this.emit('show-message', null, null, MessageType.NONE); } -- 2.55.0