From 86605518b529bb62b717961cbf9cb2b9153d1b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacy=20Kuchci=C5=84ski?= Date: Sat, 22 Nov 2025 01:45:32 +0100 Subject: [PATCH 1/4] timeLimitsManager: Factor out wellbeing-daily-limit-enabled Factor out wellbeing-daily-limit-enabled specific for the wellbeing screen time limits functionality to a separate function, which will make it easier in the future to make the daily-limit-enabled property also handle parental controls session limits functionality. Part-of: --- js/misc/timeLimitsManager.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/js/misc/timeLimitsManager.js b/js/misc/timeLimitsManager.js index e1a22f0f82..2793687dce 100644 --- a/js/misc/timeLimitsManager.js +++ b/js/misc/timeLimitsManager.js @@ -876,7 +876,7 @@ export const TimeLimitsManager = GObject.registerClass({ * @returns {number} */ _calculateDailyLimitReachedAtSecs(nowSecs, dailyLimitSecs, startOfTodaySecs) { - console.assert(this.dailyLimitEnabled, + console.assert(this.wellbeingDailyLimitEnabled, 'Daily limit reached-at time only makes sense if limits are enabled'); // NOTE: This might return -1. @@ -1045,6 +1045,15 @@ export const TimeLimitsManager = GObject.registerClass({ return this._parentalControlsManager.sessionLimitsEnabled(); } + /** + * Whether the wellbeing daily limit is enabled. + * + * @type {boolean} + */ + get wellbeingDailyLimitEnabled() { + return this._screenTimeLimitSettings.get_boolean('daily-limit-enabled'); + } + /** * The time when the daily limit will be reached. If the user is currently * active, and has not reached the limit, this is a non-zero value in the @@ -1060,7 +1069,7 @@ export const TimeLimitsManager = GObject.registerClass({ case TimeLimitsState.DISABLED: return 0; case TimeLimitsState.ACTIVE: { - if (!this.dailyLimitEnabled) + if (!this.wellbeingDailyLimitEnabled) return 0; const nowSecs = this.getCurrentTime(); @@ -1098,7 +1107,7 @@ export const TimeLimitsManager = GObject.registerClass({ * @type {boolean} */ get dailyLimitEnabled() { - return this._screenTimeLimitSettings.get_boolean('daily-limit-enabled'); + return this.wellbeingDailyLimitEnabled; } /** -- GitLab From c82589b0747770aa25a3a66c740220cda8356522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacy=20Kuchci=C5=84ski?= Date: Sat, 22 Nov 2025 04:14:10 +0100 Subject: [PATCH 2/4] timeLimitsManager: Differentiate wellbeing daily limit Rename the daily limit variables and functions in various places, so that its name makes it clear what it represents. Part-of: --- js/misc/timeLimitsManager.js | 51 +++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/js/misc/timeLimitsManager.js b/js/misc/timeLimitsManager.js index 2793687dce..17fe8b7d51 100644 --- a/js/misc/timeLimitsManager.js +++ b/js/misc/timeLimitsManager.js @@ -866,16 +866,16 @@ export const TimeLimitsManager = GObject.registerClass({ } /** - * Work out the timestamp at which the daily limit was reached. + * Work out the timestamp at which the wellbeing daily limit was reached. * - * If the user has not reached the daily limit yet today, this will return 0. + * If the user has not reached the wellbeing daily limit yet today, this will return 0. * * @param {number} nowSecs ‘Current’ time to calculate from. - * @param {number} dailyLimitSecs Daily limit in seconds. + * @param {number} wellbeingDailyLimitSecs Wellbeing daily limit in seconds. * @param {number} startOfTodaySecs Time for the start of today. * @returns {number} */ - _calculateDailyLimitReachedAtSecs(nowSecs, dailyLimitSecs, startOfTodaySecs) { + _calculateWellbeingDailyLimitReachedAtSecs(nowSecs, wellbeingDailyLimitSecs, startOfTodaySecs) { console.assert(this.wellbeingDailyLimitEnabled, 'Daily limit reached-at time only makes sense if limits are enabled'); @@ -892,16 +892,16 @@ export const TimeLimitsManager = GObject.registerClass({ else if (this._stateTransitions[i]['oldState'] === UserState.ACTIVE) activeTimeTodaySecs += Math.max(this._stateTransitions[i]['wallTimeSecs'] - activeStartTimeSecs, 0); - if (activeTimeTodaySecs >= dailyLimitSecs) - return this._stateTransitions[i]['wallTimeSecs'] - (activeTimeTodaySecs - dailyLimitSecs); + if (activeTimeTodaySecs >= wellbeingDailyLimitSecs) + return this._stateTransitions[i]['wallTimeSecs'] - (activeTimeTodaySecs - wellbeingDailyLimitSecs); } if (this._stateTransitions.length > 0 && this._stateTransitions.at(-1)['newState'] === UserState.ACTIVE) activeTimeTodaySecs += Math.max(nowSecs - activeStartTimeSecs, 0); - if (activeTimeTodaySecs >= dailyLimitSecs) - return nowSecs - (activeTimeTodaySecs - dailyLimitSecs); + if (activeTimeTodaySecs >= wellbeingDailyLimitSecs) + return nowSecs - (activeTimeTodaySecs - wellbeingDailyLimitSecs); // Limit not reached yet. return 0; @@ -948,14 +948,15 @@ export const TimeLimitsManager = GObject.registerClass({ console.debug('TimeLimitsManager: Parental controls session limits: ' + `${sessionLimitsDebug}`); - // Work out how much time the user has spent at the screen today. + // Work out how much time the user has spent at the screen today + // for the wellbeing daily limit calculations. const activeTimeTodaySecs = this._calculateActiveTimeTodaySecs(nowSecs, startOfTodaySecs); - const dailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); - const dailyLimitEnabled = this._screenTimeLimitSettings.get_boolean('daily-limit-enabled'); + const wellbeingDailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); + const wellbeingDailyLimitEnabled = this._screenTimeLimitSettings.get_boolean('daily-limit-enabled'); - const dailyLimitDebug = dailyLimitEnabled ? `${dailyLimitSecs}s` : 'disabled'; + const wellbeingDailyLimitDebug = wellbeingDailyLimitEnabled ? `${wellbeingDailyLimitSecs}s` : 'disabled'; console.debug('TimeLimitsManager: Wellbeing active time today: ' + - `${activeTimeTodaySecs}s, daily limit ${dailyLimitDebug}`); + `${activeTimeTodaySecs}s, daily limit ${wellbeingDailyLimitDebug}`); // Update TimeLimitsState. When the user is inactive, there's no point @@ -979,7 +980,7 @@ export const TimeLimitsManager = GObject.registerClass({ // Schedule an update for when we expect the session limit will be reached. this._scheduleUpdateState(currentSessionEnd - nowSecs); } - } else if (dailyLimitEnabled && activeTimeTodaySecs >= dailyLimitSecs) { + } else if (wellbeingDailyLimitEnabled && activeTimeTodaySecs >= wellbeingDailyLimitSecs) { newState = TimeLimitsState.LIMIT_REACHED; // Schedule an update for when the daily limit will be reset again. @@ -987,9 +988,9 @@ export const TimeLimitsManager = GObject.registerClass({ } else if (this._userState === UserState.ACTIVE) { newState = TimeLimitsState.ACTIVE; - // Schedule an update for when we expect the daily limit will be reached. - if (dailyLimitEnabled) - this._scheduleUpdateState(dailyLimitSecs - activeTimeTodaySecs); + // Schedule an update for when we expect the wellbeing daily limit will be reached. + if (wellbeingDailyLimitEnabled) + this._scheduleUpdateState(wellbeingDailyLimitSecs - activeTimeTodaySecs); } else { // User is inactive, so no point scheduling anything until they become // active again. @@ -1075,25 +1076,27 @@ export const TimeLimitsManager = GObject.registerClass({ const nowSecs = this.getCurrentTime(); const [startOfTodaySecs] = this._getStartOfTodaySecs(nowSecs); const activeTimeTodaySecs = this._calculateActiveTimeTodaySecs(nowSecs, startOfTodaySecs); - const dailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); + const wellbeingDailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); - console.assert(dailyLimitSecs >= activeTimeTodaySecs, 'Active time unexpectedly high'); + console.assert(wellbeingDailyLimitSecs >= activeTimeTodaySecs, 'Active time unexpectedly high'); if (this._userState === UserState.ACTIVE) - return nowSecs + (dailyLimitSecs - activeTimeTodaySecs); + return nowSecs + (wellbeingDailyLimitSecs - activeTimeTodaySecs); else return 0; } case TimeLimitsState.LIMIT_REACHED: { const nowSecs = this.getCurrentTime(); const [startOfTodaySecs] = this._getStartOfTodaySecs(nowSecs); - const dailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); - const dailyLimitReachedAtSecs = this._calculateDailyLimitReachedAtSecs(nowSecs, dailyLimitSecs, startOfTodaySecs); + const wellbeingDailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); + const wellbeingDailyLimitReachedAtSecs = + this._calculateWellbeingDailyLimitReachedAtSecs(nowSecs, + wellbeingDailyLimitSecs, startOfTodaySecs); - console.assert(dailyLimitReachedAtSecs > 0, + console.assert(wellbeingDailyLimitReachedAtSecs > 0, 'Daily limit reached-at unexpectedly low'); - return dailyLimitReachedAtSecs; + return wellbeingDailyLimitReachedAtSecs; } default: console.assert(false, `Unexpected state ${this._state}`); -- GitLab From cb5bd37e3b5fcb60228861760c3dd310a50de53c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacy=20Kuchci=C5=84ski?= Date: Sat, 22 Nov 2025 01:57:26 +0100 Subject: [PATCH 3/4] timeLimitsManager: Differentiate malcontentSessionLimits Make the parental controls session limits related variables and functions have more specific names, more representative of their contents and function. Part-of: --- js/misc/timeLimitsManager.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/js/misc/timeLimitsManager.js b/js/misc/timeLimitsManager.js index 17fe8b7d51..4458d3bbc9 100644 --- a/js/misc/timeLimitsManager.js +++ b/js/misc/timeLimitsManager.js @@ -931,10 +931,11 @@ export const TimeLimitsManager = GObject.registerClass({ if (startOfTodaySecs > this._lastStateChangeTimeSecs) newState = TimeLimitsState.ACTIVE; + // Work out estimated times for the parental controls session limits calculations. const [, currentSessionStart, currentSessionEnd, nextSessionStart] = this._estimatedTimes; - const sessionLimitsEnabled = this._parentalControlsManager.sessionLimitsEnabled(); + const parentalControlsSessionLimitsEnabled = this._parentalControlsManager.sessionLimitsEnabled(); - if (sessionLimitsEnabled) { + if (parentalControlsSessionLimitsEnabled) { // Parental controls session limits have either just been enabled // and the estimated times have not been cached yet, or disabled // but the manager has not detected that yet, so skip updating state. @@ -942,11 +943,11 @@ export const TimeLimitsManager = GObject.registerClass({ return; } - const sessionLimitsDebug = sessionLimitsEnabled ? 'current session start: ' + + const parentalControlsSessionLimitsDebug = parentalControlsSessionLimitsEnabled ? 'current session start: ' + `${this._unixToString(currentSessionStart)}, current session end: ` + `${this._unixToString(currentSessionEnd)}` : 'disabled'; console.debug('TimeLimitsManager: Parental controls session limits: ' + - `${sessionLimitsDebug}`); + `${parentalControlsSessionLimitsDebug}`); // Work out how much time the user has spent at the screen today // for the wellbeing daily limit calculations. @@ -961,7 +962,7 @@ export const TimeLimitsManager = GObject.registerClass({ // Update TimeLimitsState. When the user is inactive, there's no point // scheduling anything until they become active again. - if (sessionLimitsEnabled) { + if (parentalControlsSessionLimitsEnabled) { if (nextSessionStart <= nowSecs) { // Just entered daily schedule or a new day, so update cached // estimated times, which will perform state update afterwards. @@ -1042,7 +1043,7 @@ export const TimeLimitsManager = GObject.registerClass({ * * @type {boolean} */ - get sessionLimitsEnabled() { + get parentalControlsSessionLimitsEnabled() { return this._parentalControlsManager.sessionLimitsEnabled(); } @@ -1200,7 +1201,7 @@ class TimeLimitsDispatcher extends GObject.Object { } case TimeLimitsState.LIMIT_REACHED: { - if (this._manager.sessionLimitsEnabled) { + if (this._manager.parentalControlsSessionLimitsEnabled) { // Trying to lock the screen will fail if the action is not // available. This happens when admin disables lock screen, // or when not running under GDM/logind. Those are corner cases @@ -1380,7 +1381,7 @@ class TimeLimitsNotificationSource extends GObject.Object { // Notify the user that they’ve reached their limit, when we // transition from any state to LIMIT_REACHED. Noop if parental // controls session limits are enabled, since we’re locking then. - if (!this._manager.sessionLimitsEnabled && + if (!this._manager.parentalControlsSessionLimitsEnabled && this._previousState !== TimeLimitsState.LIMIT_REACHED) { this._ensureNotification({ title: _('Screen Time Limit Reached'), -- GitLab From a285b8edffaeb5143c4849d5a72f946ead6550a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacy=20Kuchci=C5=84ski?= Date: Sat, 22 Nov 2025 03:43:17 +0100 Subject: [PATCH 4/4] timeLimitsManager: Implement malcontent notification Implement 'time is almost up' notification for the parental controls session limits functionality. Closes: https://gitlab.gnome.org/GNOME/gnome-shell/-/work_items/8576 Part-of: --- js/misc/timeLimitsManager.js | 69 ++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/js/misc/timeLimitsManager.js b/js/misc/timeLimitsManager.js index 4458d3bbc9..abaf3ba249 100644 --- a/js/misc/timeLimitsManager.js +++ b/js/misc/timeLimitsManager.js @@ -42,6 +42,7 @@ const TimerChildProxy = Gio.DBusProxy.makeProxyWrapper(TimerChildIface); export const HISTORY_THRESHOLD_SECONDS = 14 * 7 * 24 * 60 * 60; // maximum time history entries are kept const LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS = 10 * 60; // notify the user 10min before their limit is reached +const PARENTAL_CONTROLS_LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS = 60; // notify the child 60s before their limit is reached const GRAYSCALE_FADE_TIME_SECONDS = 3; const GRAYSCALE_SATURATION = 1.0; // saturation ([0.0, 1.0]) when grayscale mode is activated, 1.0 means full desaturation @@ -175,8 +176,11 @@ export const TimeLimitsManager = GObject.registerClass({ this._parentalControlsManager = this._parentalControlsManagerFactory.new(); this._parentalControlsManager.connectObject( - 'session-limits-changed', () => this._onSessionLimitsChanged().catch(logError), - this); + 'session-limits-changed', () => { + this._onSessionLimitsChanged().catch(logError); + this.notify('daily-limit-time'); + this.notify('daily-limit-enabled'); + }, this); this._estimatedTimes = []; this._timerChildProxyFactory = timerChildProxyFactory ?? { @@ -295,6 +299,7 @@ export const TimeLimitsManager = GObject.registerClass({ this._estimatedTimes = []; this._updateState(); + this.notify('daily-limit-time'); } _storingTransitionsEnabled() { @@ -1064,9 +1069,22 @@ export const TimeLimitsManager = GObject.registerClass({ * limit, or if time limits are disabled, this is zero. * It’s measured in real time seconds. * + * Considers both the parental controls and wellbeing limits. + * * @type {number} */ get dailyLimitTime() { + // Check parental controls session limits + if (this.parentalControlsSessionLimitsEnabled) { + if (this._state === TimeLimitsState.DISABLED || + this._userState === UserState.INACTIVE) + return 0; + + const [, , currentSessionEnd] = this._estimatedTimes; + return currentSessionEnd; + } + + // Handle wellbeing daily limit otherwise switch (this._state) { case TimeLimitsState.DISABLED: return 0; @@ -1108,10 +1126,13 @@ export const TimeLimitsManager = GObject.registerClass({ /** * Whether the daily limit is enabled. * + * Considers both the parental controls and wellbeing limits. + * * @type {boolean} */ get dailyLimitEnabled() { - return this.wellbeingDailyLimitEnabled; + return this.parentalControlsSessionLimitsEnabled || + this.wellbeingDailyLimitEnabled; } /** @@ -1349,26 +1370,46 @@ class TimeLimitsNotificationSource extends GObject.Object { const remainingSecs = limitDueTime - currentTime; console.debug(`TimeLimitsNotificationSource: ${remainingSecs}s left before limit is reached`); - if (remainingSecs > LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS) { + let timeoutLimit; + + if (this._manager.parentalControlsSessionLimitsEnabled) + timeoutLimit = PARENTAL_CONTROLS_LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS; + else + timeoutLimit = LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS; + + if (remainingSecs > timeoutLimit) { this._notification?.destroy(); this._notification = null; // Schedule to show a notification when the upcoming notification // time is reached. - this._scheduleUpdateState(remainingSecs - LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS); + this._scheduleUpdateState(remainingSecs - timeoutLimit); break; - } else if (Math.ceil(remainingSecs) === LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS) { - // Bang on time to show this notification. - const remainingMinutes = Math.floor(LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS / 60); - const titleText = Gettext.ngettext( - 'Screen Time Limit in %d Minute', - 'Screen Time Limit in %d Minutes', - remainingMinutes - ).format(remainingMinutes); + } else if (Math.ceil(remainingSecs) === timeoutLimit) { + let remainingTime, titleText, bodyText; + + if (this._manager.parentalControlsSessionLimitsEnabled) { + remainingTime = timeoutLimit; + titleText = _('Screen Time Limit is almost up'); + bodyText = Gettext.ngettext( + 'The computer will lock in %d second.', + 'The computer will lock in %d seconds.', + remainingTime + ).format(remainingTime); + } else { + // Bang on time to show this notification. + remainingTime = Math.floor(timeoutLimit / 60); + titleText = Gettext.ngettext( + 'Screen Time Limit in %d Minute', + 'Screen Time Limit in %d Minutes', + remainingTime + ).format(remainingTime); + bodyText = _('Your screen time limit is approaching'); + } this._ensureNotification({ title: titleText, - body: _('Your screen time limit is approaching'), + body: bodyText, urgency: MessageTray.Urgency.HIGH, }); this._source.addNotification(this._notification); -- GitLab