From 2c136b6c10b3f4e172cbe181ab794b08b0741ef1 Mon Sep 17 00:00:00 2001 From: Mattias <810218+avesst@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:37:24 +0200 Subject: [PATCH] notifications: auto-enable do not disturb while screen sharing (#3316) Turn on do not disturb while a screen share is active so notification popups and their contents stay off the shared screen. The unread dot and the existing dnd bypasses are unaffected. Detection reuses the screen sharing privacy indicator: niri cast ipc, otherwise pipewire and xdg-desktop-portal. Neither source says when it has finished reporting, so a share reading as gone is only acted on once it stays gone for four seconds. Engaging stays immediate, and turning the setting off releases right away since that is a user action rather than a detection guess. Turning dnd off by hand during a share is remembered against the sources that were sharing at the time, so it holds for that share and across a shell reload. A share started after that one has ended gets a fresh decision. Co-authored-by: Claude Opus 5 --- quickshell/Common/SessionData.qml | 128 +++++++++++++++++- quickshell/Common/SettingsData.qml | 1 + quickshell/Common/settings/SessionSpec.js | 3 + quickshell/Common/settings/SettingsSpec.js | 1 + .../Modules/Settings/NotificationsTab.qml | 9 ++ quickshell/Services/PrivacyService.qml | 31 +++++ .../translations/settings_search_index.json | 26 ++++ 7 files changed, 196 insertions(+), 3 deletions(-) diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index aa5b2d000..5a97467c4 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -36,6 +36,9 @@ Singleton { property bool isLightMode: false property bool doNotDisturb: false property real doNotDisturbUntil: 0 + property bool doNotDisturbHeldByScreenShare: false + property bool screenShareDndDismissed: false + property var screenShareDismissedIds: [] property bool idleInhibited: false property real idleInhibitedUntil: 0 property string terminalOverride: "" @@ -72,7 +75,15 @@ Singleton { id: dndExpireTimer repeat: false running: false - onTriggered: root.setDoNotDisturb(false) + onTriggered: root.setDoNotDisturb(false, 0, "timer") + } + + Timer { + id: screenShareReleaseTimer + interval: 4000 + repeat: false + running: false + onTriggered: root._endScreenShareDnd() } function _armDndExpireTimer() { @@ -81,7 +92,7 @@ Singleton { return; const remaining = doNotDisturbUntil - Date.now(); if (remaining <= 0) { - setDoNotDisturb(false); + setDoNotDisturb(false, 0, "timer"); return; } dndExpireTimer.interval = remaining; @@ -109,6 +120,21 @@ Singleton { root.suppressOSD = true; osdSuppressTimer.restart(); root._applyDndExpirySanity(); + Qt.callLater(root._syncScreenShareDnd); + } + } + + Connections { + target: (!root.isGreeterMode && typeof SettingsData !== "undefined" && SettingsData.notificationDndWhileScreenSharing && typeof PrivacyService !== "undefined") ? PrivacyService : null + function onScreensharingActiveChanged() { + root._syncScreenShareDnd(); + } + } + + Connections { + target: typeof SettingsData !== "undefined" ? SettingsData : null + function onNotificationDndWhileScreenSharingChanged() { + root._syncScreenShareDnd(); } } @@ -311,6 +337,7 @@ Singleton { Theme.generateSystemThemesFromCurrentTheme(); loaded(); + Qt.callLater(root._syncScreenShareDnd); _checkSessionWritable(); } catch (e) { @@ -393,6 +420,7 @@ Singleton { Theme.generateSystemThemesFromCurrentTheme(); loaded(); + Qt.callLater(root._syncScreenShareDnd); } catch (e) { _parseError = true; const msg = e.message; @@ -645,11 +673,24 @@ Singleton { }); } - function setDoNotDisturb(enabled, durationMinutes) { + function setDoNotDisturb(enabled, durationMinutes, origin) { + const src = origin || "user"; + if (src === "user") { + if (!enabled && _screenShareShouldHold()) { + screenShareDndDismissed = true; + screenShareDismissedIds = _screencastIds(); + } + if (doNotDisturbHeldByScreenShare) + doNotDisturbHeldByScreenShare = false; + } + const minutes = Number(durationMinutes) || 0; doNotDisturb = enabled; doNotDisturbUntil = (enabled && minutes > 0) ? Date.now() + minutes * 60 * 1000 : 0; saveSettings(); + + if (src !== "screenshare") + Qt.callLater(_syncScreenShareDnd); } function setIdleInhibited(enabled, durationMinutes) { @@ -669,11 +710,92 @@ Singleton { setDoNotDisturb(false); return; } + if (doNotDisturbHeldByScreenShare) + doNotDisturbHeldByScreenShare = false; doNotDisturb = true; doNotDisturbUntil = target; saveSettings(); } + function _screenShareShouldHold() { + if (isGreeterMode) + return false; + if (typeof SettingsData === "undefined" || !SettingsData.notificationDndWhileScreenSharing) + return false; + if (typeof PrivacyService === "undefined" || !PrivacyService.screensharingActive) + return false; + return true; + } + + function _screencastIds() { + return (typeof PrivacyService !== "undefined") ? PrivacyService.screencastSourceIds() : []; + } + + function _dismissalApplies() { + if (!screenShareDndDismissed) + return false; + const dismissed = screenShareDismissedIds || []; + if (!dismissed.length) + return true; + const current = _screencastIds(); + if (!current.length) + return true; + return current.some(id => dismissed.indexOf(id) !== -1); + } + + function _releaseScreenShareDndHold() { + if (!doNotDisturbHeldByScreenShare) + return; + doNotDisturbHeldByScreenShare = false; + if (!doNotDisturb || doNotDisturbUntil > 0) { + saveSettings(); + return; + } + setDoNotDisturb(false, 0, "screenshare"); + } + + function _endScreenShareDnd() { + _clearScreenShareDismissal(); + _releaseScreenShareDndHold(); + } + + function _clearScreenShareDismissal() { + if (!screenShareDndDismissed) + return; + screenShareDndDismissed = false; + screenShareDismissedIds = []; + saveSettings(); + } + + function _syncScreenShareDnd() { + if (isGreeterMode) + return; + + if (!_screenShareShouldHold()) { + if (!doNotDisturbHeldByScreenShare && !screenShareDndDismissed) + return; + const settingOn = typeof SettingsData !== "undefined" && SettingsData.notificationDndWhileScreenSharing; + if (!settingOn) { + screenShareReleaseTimer.stop(); + _endScreenShareDnd(); + return; + } + screenShareReleaseTimer.restart(); + return; + } + + screenShareReleaseTimer.stop(); + + if (!_dismissalApplies()) + _clearScreenShareDismissal(); + + if (screenShareDndDismissed || doNotDisturb) + return; + + doNotDisturbHeldByScreenShare = true; + setDoNotDisturb(true, 0, "screenshare"); + } + function setWallpaper(imagePath) { wallpaperPath = imagePath; if (perModeWallpaper) { diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 0e2988031..b0f1d3349 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -988,6 +988,7 @@ Singleton { property bool notificationHistorySaveCritical: true property var notificationRules: [] property bool notificationDndAllowCritical: true + property bool notificationDndWhileScreenSharing: true property bool notificationFocusedMonitor: false // Island is a per-instance render mode: any bar config with island:true draws as an island // instead of a DankBar, and carries its own island* look settings. diff --git a/quickshell/Common/settings/SessionSpec.js b/quickshell/Common/settings/SessionSpec.js index be5c27912..cc1324808 100644 --- a/quickshell/Common/settings/SessionSpec.js +++ b/quickshell/Common/settings/SessionSpec.js @@ -4,6 +4,9 @@ var SPEC = { isLightMode: { def: false }, doNotDisturb: { def: false }, doNotDisturbUntil: { def: 0 }, + doNotDisturbHeldByScreenShare: { def: false }, + screenShareDndDismissed: { def: false }, + screenShareDismissedIds: { def: [] }, idleInhibited: { def: false }, idleInhibitedUntil: { def: 0 }, terminalOverride: { def: "" }, diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index cb89eafb2..27453b4e4 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -533,6 +533,7 @@ var SPEC = { notificationHistorySaveCritical: { def: true }, notificationRules: { def: [] }, notificationDndAllowCritical: { def: true }, + notificationDndWhileScreenSharing: { def: true }, notificationFocusedMonitor: { def: false }, diff --git a/quickshell/Modules/Settings/NotificationsTab.qml b/quickshell/Modules/Settings/NotificationsTab.qml index f6322d9fb..b996ba425 100644 --- a/quickshell/Modules/Settings/NotificationsTab.qml +++ b/quickshell/Modules/Settings/NotificationsTab.qml @@ -429,6 +429,15 @@ Item { onToggled: checked => SessionData.setDoNotDisturb(checked) } + SettingsToggleRow { + settingKey: "notificationDndWhileScreenSharing" + tags: ["notification", "dnd", "screenshare", "sharing", "privacy"] + text: I18n.tr("Automatically enable while screen sharing", "do not disturb auto enable while screen sharing toggle") + description: I18n.tr("Turn on Do Not Disturb while a screen share is active", "do not disturb auto enable while screen sharing description") + checked: SettingsData.notificationDndWhileScreenSharing + onToggled: checked => SettingsData.set("notificationDndWhileScreenSharing", checked) + } + SettingsToggleRow { settingKey: "notificationDndAllowCritical" tags: ["notification", "dnd", "critical", "priority", "urgent", "bypass"] diff --git a/quickshell/Services/PrivacyService.qml b/quickshell/Services/PrivacyService.qml index 49088621d..1c0c12f36 100644 --- a/quickshell/Services/PrivacyService.qml +++ b/quickshell/Services/PrivacyService.qml @@ -126,6 +126,37 @@ Singleton { return /xdg-desktop-portal|xdpw|screencast|screen-cast|screen|gnome shell|kwin|obs|niri/.test(combined) } + function screencastSourceIds() { + const ids = [] + + if (CompositorService.isNiri) { + for (const cast of NiriService.casts) { + if (cast && cast.is_active) { + ids.push("niri:" + cast.stream_id) + } + } + } + + if (!Pipewire.ready || !Pipewire.nodes?.values) { + return ids + } + + for (let i = 0; i < Pipewire.nodes.values.length; i++) { + const node = Pipewire.nodes.values[i] + if (!node || !node.ready) { + continue + } + + const isVideoSource = (node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource + const isVideoStream = node.properties && node.properties["media.class"] === "Stream/Output/Video" + if ((isVideoSource || isVideoStream) && looksLikeScreencast(node)) { + ids.push("pw:" + node.id) + } + } + + return ids.sort() + } + function getMicrophoneStatus() { return microphoneActive ? "active" : "inactive" } diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 39616545b..4e3b4fed4 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -7128,6 +7128,32 @@ ], "description": "Control animation duration for notification popups and history" }, + { + "section": "notificationDndWhileScreenSharing", + "label": "Automatically enable while screen sharing", + "tabIndex": 17, + "category": "Notifications", + "keywords": [ + "active", + "alerts", + "automatically", + "disturb", + "dnd", + "enable", + "messages", + "notification", + "notifications", + "privacy", + "screen", + "screenshare", + "share", + "sharing", + "toast", + "turn", + "while" + ], + "description": "Turn on Do Not Disturb while a screen share is active" + }, { "section": "notificationPopupBodyInvokesAction", "label": "Body Click Runs Default Action",