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 <[email protected]>
This commit is contained in:
Mattias
2026-09-15 12:37:24 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent 6e9a657132
commit 2c136b6c10
7 changed files with 196 additions and 3 deletions
+125 -3
View File
@@ -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) {
+1
View File
@@ -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.
@@ -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: "" },
@@ -533,6 +533,7 @@ var SPEC = {
notificationHistorySaveCritical: { def: true },
notificationRules: { def: [] },
notificationDndAllowCritical: { def: true },
notificationDndWhileScreenSharing: { def: true },
notificationFocusedMonitor: { def: false },
@@ -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"]
+31
View File
@@ -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"
}
@@ -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",