Merge branch 'extend_notification_api' into 'main'

Draft: GtkNotificationDaemon: Extend API to include new portal features

Closes #6983

See merge request GNOME/gnome-shell!3389
This commit is contained in:
Julian Sparber 2024-06-29 06:09:46 +00:00
commit 8e482af5d2
14 changed files with 264 additions and 154 deletions

View file

@ -9,5 +9,12 @@
<arg name="app_id" type="s" direction="in"/>
<arg name="id" type="s" direction="in"/>
</method>
<signal name="ActionInvoked">
<arg name="app_id" type="s"/>
<arg name="id" type="s"/>
<arg name="action" type="s"/>
<arg name="parameter" type="av"/>
<arg name="platform_data" type="a{sv}"/>
</signal>
</interface>
</node>

View file

@ -1081,7 +1081,7 @@ export const LoginDialog = GObject.registerClass({
title: _('Login Attempt Timed Out'),
body: _('Login took too long, please try again'),
urgency: MessageTray.Urgency.CRITICAL,
isTransient: true,
displayHint: MessageTray.DisplayHint.TRANSIENT,
});
this._conflictingSessionNotification.connect('destroy', () => {
this._conflictingSessionNotification = null;

View file

@ -1,11 +1,10 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import * as MessageTray from './messageTray.js';
import Shell from 'gi://Shell';
import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
import * as Signals from '../misc/signals.js';
import * as Main from './main.js';
// In alphabetical order
const RENAMED_DESKTOP_IDS = {
'baobab.desktop': 'org.gnome.baobab.desktop',
@ -161,13 +160,11 @@ class AppFavorites extends Signals.EventEmitter {
if (!this._addFavorite(appId, pos))
return;
let app = Shell.AppSystem.get_default().lookup_app(appId);
const app = Shell.AppSystem.get_default().lookup_app(appId);
let msg = _('%s has been pinned to the dash.').format(app.get_name());
Main.overview.setMessage(msg, {
forFeedback: true,
undoCallback: () => this._removeFavorite(appId),
});
this.showNotification(_('%s has been pinned to the dash.').format(app.get_name()),
null,
() => this._removeFavorite(appId));
}
addFavorite(appId) {
@ -196,11 +193,21 @@ class AppFavorites extends Signals.EventEmitter {
if (!this._removeFavorite(appId))
return;
let msg = _('%s has been unpinned from the dash.').format(app.get_name());
Main.overview.setMessage(msg, {
forFeedback: true,
undoCallback: () => this._addFavorite(appId, pos),
this.showNotification(_('%s has been unpinned from the dash.').format(app.get_name()),
null,
() => this._addFavorite(appId, pos));
}
showNotification(title, body, undoCallback) {
const source = MessageTray.getSystemSource();
const notification = new MessageTray.Notification({
source,
title,
body,
displayHint: MessageTray.DisplayHint.TRANSIENT | MessageTray.DisplayHint.FORCE_IMMEDIATELY,
});
notification.addAction(_('Undo'), () => undoCallback());
source.addNotification(notification);
}
}

View file

@ -801,6 +801,11 @@ class NotificationMessage extends MessageList.Message {
notification.bind_property('gicon',
this, 'icon',
GObject.BindingFlags.SYNC_CREATE);
notification.bind_property_full('display-hint',
this, 'can-close',
GObject.BindingFlags.SYNC_CREATE,
(bind, value) => [true, !(value & MessageTray.DisplayHint.PERSISTENT)],
null);
this._actions = new Map();
this.notification.actions.forEach(action => {
@ -812,10 +817,6 @@ class NotificationMessage extends MessageList.Message {
this.notification.activate();
}
canClose() {
return true;
}
_addAction(action) {
if (!this._buttonBox) {
this._buttonBox = new St.BoxLayout({

View file

@ -278,7 +278,7 @@ async function _initializeUI() {
source,
title: _('System was put in unsafe mode'),
body: _('Apps now have unrestricted access'),
isTransient: true,
displayHint: MessageTray.DisplayHint.TRANSIENT,
});
notification.addAction(_('Undo'),
() => (global.context.unsafe_mode = false));
@ -620,7 +620,7 @@ export function notify(msg, details) {
source,
title: msg,
body: details,
isTransient: true,
displayHint: MessageTray.DisplayHint.TRANSIENT,
});
source.addNotification(notification);
}

View file

@ -405,6 +405,10 @@ export const Message = GObject.registerClass({
'datetime', 'datetime', 'datetime',
GObject.ParamFlags.READWRITE,
GLib.DateTime),
'can-close': GObject.ParamSpec.boolean(
'can-close', 'can-close', 'can-close',
GObject.ParamFlags.READWRITE,
false),
},
Signals: {
'close': {},
@ -487,7 +491,8 @@ export const Message = GObject.registerClass({
this.connect('destroy', this._onDestroy.bind(this));
this._header.closeButton.connect('clicked', this.close.bind(this));
this._header.closeButton.visible = this.canClose();
this.bind_property('can-close', this._header.closeButton, 'visible', GObject.BindingFlags.SYNC_CREATE);
this._header.expandButton.connect('clicked', () => {
if (this.expanded)
@ -633,10 +638,6 @@ export const Message = GObject.registerClass({
this.emit('unexpanded');
}
canClose() {
return false;
}
_onDestroy() {
}
@ -646,7 +647,7 @@ export const Message = GObject.registerClass({
if (keysym === Clutter.KEY_Delete ||
keysym === Clutter.KEY_KP_Delete ||
keysym === Clutter.KEY_BackSpace) {
if (this.canClose()) {
if (this.canClose) {
this.close();
return Clutter.EVENT_STOP;
}
@ -816,7 +817,7 @@ export const MessageListSection = GObject.registerClass({
}
clear() {
let messages = this._messages.filter(msg => msg.canClose());
let messages = this._messages.filter(msg => msg.canClose);
// If there are few messages, letting them all zoom out looks OK
if (messages.length < 2) {
@ -854,7 +855,7 @@ export const MessageListSection = GObject.registerClass({
this.notify('empty');
}
let canClear = messages.some(m => m.canClose());
let canClear = messages.some(m => m.canClose);
if (this._canClear !== canClear) {
this._canClear = canClear;
this.notify('can-clear');

View file

@ -78,6 +78,25 @@ export const PrivacyScope = {
SYSTEM: 1,
};
/** @enum {number} */
export const DisplayHint = {
NONE: 0,
// Only show a banner for the message.
TRANSIENT: 1 << 0,
// Don't show a banner but keep the message till dismissed.
NO_BANNER: 1 << 1,
// Show a banner immediately.
FORCE_IMMEDIATELY: 1 << 2,
// Message that can't be dismissed by the user.
PERSISTENT: 1 << 3,
// Message that won't automatically dismissed when an action is invoked.
RESIDENT: 1 << 4,
// Show on the lock screen that a source as a message, but without any content.
SHOW_ON_LOCKSCREEN: 1 << 5,
// Show the message on the lock screen.
SHOW_CONTENT_ON_LOCKSCREEN: 1 << 6,
};
class FocusGrabber {
constructor(actor) {
this._actor = actor;
@ -314,11 +333,43 @@ export const NotificationApplicationPolicy = GObject.registerClass({
export const Sound = GObject.registerClass(
class Sound extends GObject.Object {
constructor(file, themedName) {
super();
static newForFile(file) {
const sound = new Sound();
sound._soundFile = file;
return sound;
}
this._soundFile = file;
this._soundName = themedName;
static newForName(name) {
const sound = new Sound();
sound._soundName = name;
return sound;
}
static newForBytes(bytes) {
const sound = new Sound();
sound._soundBytes = bytes;
return sound;
}
static deserialize(serializedSound) {
const sound = serializedSound?.unpack();
if (!sound || sound === 'silent')
return null;
if (sound === 'default')
return Sound.newForName('message');
if (sound.length !== 2)
return null;
const [type, data] = sound.map(v => v.unpack());
if (type === 'file')
return Sound.newForFile(Gio.File.new_for_commandline_arg(data.unpack()));
if (type === 'bytes')
return Sound.newForBytes(data.unpack());
return null;
}
play() {
@ -328,6 +379,22 @@ class Sound extends GObject.Object {
player.play_from_theme(this._soundName, _('Notification sound'), null);
else if (this._soundFile)
player.play_from_file(this._soundFile, _('Notification sound'), null);
else if (this._soundBytes)
this._playSoundBytes();
}
async _playSoundBytes() {
const player = global.display.get_sound_player();
const [file, stream] = Gio.File.new_tmp('XXXXXX-notification-sound');
await stream.output_stream.write_bytes_async(this._soundBytes,
GLib.PRIORITY_DEFAULT, null);
stream.close_async(GLib.PRIORITY_DEFAULT, null);
player.play_from_file(file, _('Notification sound'), null);
this._tempFile = file;
}
cleanupTempFile() {
this._tempFile?.delete(null);
}
});
@ -408,6 +475,19 @@ export class Notification extends GObject.Object {
this.notify('privacy-scope');
}
get displayHint() {
return this._displayHint;
}
set displayHint(displayHint) {
// TODO: check if the value is in range
if (this._displayHint === displayHint)
return;
this._displayHint = displayHint;
this.notify('display-hint');
}
get urgency() {
return this._urgency;
}
@ -434,7 +514,7 @@ export class Notification extends GObject.Object {
// because it is common for such notifications to update themselves with new
// information based on the action. We'd like to display the updated information
// in place, rather than pop-up a new notification.
if (this.resident)
if (this.displayHint & DisplayHint.RESIDENT)
return;
this.destroy();
@ -467,13 +547,14 @@ export class Notification extends GObject.Object {
// because it is common for such notifications to update themselves with new
// information based on the action. We'd like to display the updated information
// in place, rather than pop-up a new notification.
if (this.resident)
if (this.displayHint & DisplayHint.RESIDENT)
return;
this.destroy();
}
destroy(reason = NotificationDestroyedReason.DISMISSED) {
this.sound?.cleanupTempFile();
this.emit('destroy', reason);
if (this._updateDatetimeId)
@ -570,13 +651,16 @@ export const Source = GObject.registerClass({
this.countUpdated();
// If acknowledged was set to false try to show the notification again
if (!notification.acknowledged)
if (!notification.acknowledged && !(notification.displayHint & DisplayHint.NO_BANNER))
this.emit('notification-request-banner', notification);
});
this.notifications.push(notification);
this.emit('notification-added', notification);
this.emit('notification-request-banner', notification);
if (!(notification.displayHint & DisplayHint.NO_BANNER))
this.emit('notification-request-banner', notification);
this.countUpdated();
}
@ -597,13 +681,6 @@ export const Source = GObject.registerClass({
// To be overridden by subclasses
open() {
}
destroyNonResidentNotifications() {
for (let i = this.notifications.length - 1; i >= 0; i--) {
if (!this.notifications[i].resident)
this.notifications[i].destroy();
}
}
});
SignalTracker.registerDestroyableType(Source);
@ -669,6 +746,11 @@ GObject.registerClass({
'is-transient', 'is-transient', 'is-transient',
GObject.ParamFlags.READWRITE,
false),
'display-hint': GObject.ParamSpec.int(
'display-hint', 'display-hint', 'display-hint',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
0, GLib.MAXINT32,
DisplayHint.NONE),
},
Signals: {
'action-added': {param_types: [Action]},
@ -1063,7 +1145,9 @@ export const MessageTray = GObject.registerClass({
let nextNotification = this._notificationQueue[0] || null;
if (hasNotifications && nextNotification) {
let limited = this._busy || Main.layoutManager.primaryMonitor.inFullscreen;
let showNextNotification = !limited || nextNotification.forFeedback || nextNotification.urgency === Urgency.CRITICAL;
let showNextNotification = !limited ||
nextNotification.displayHint & DisplayHint.FORCE_IMMEDIATELY ||
nextNotification.urgency === Urgency.CRITICAL;
if (showNextNotification)
this._showNotification();
}
@ -1254,7 +1338,7 @@ export const MessageTray = GObject.registerClass({
_hideNotificationCompleted() {
let notification = this._notification;
this._notification = null;
if (!this._notificationRemoved && notification.isTransient)
if (!this._notificationRemoved && notification.displayHint & DisplayHint.TRANSIENT)
notification.destroy(NotificationDestroyedReason.EXPIRED);
this._pointerInNotification = false;

View file

@ -197,16 +197,18 @@ class FdoNotificationDaemon {
const gicon = this._imageForNotificationData(hints);
const soundFile = 'sound-file' in hints
? Gio.File.new_for_path(hints['sound-file']) : null;
let sound;
if ('sound-file' in hints)
sound = MessageTray.Sound.newForFile(Gio.File.new_for_path(hints['sound-file']));
else if ('sound-name' in hints)
sound = MessageTray.Sound.newForName(hints['sound-name']);
notification.set({
title: summary,
body,
gicon,
useBodyMarkup: true,
sound: new MessageTray.Sound(soundFile, hints['sound-name']),
acknowledged: false,
sound,
});
notification.clearActions();
@ -248,15 +250,20 @@ class FdoNotificationDaemon {
notification.urgency = MessageTray.Urgency.CRITICAL;
break;
}
notification.resident = !!hints.resident;
if (hints.resident)
notification.displayHint |= MessageTray.DisplayHint.RESIDENT;
// 'transient' is a reserved keyword in JS, so we have to retrieve the value
// of the 'transient' hint with hints['transient'] rather than hints.transient
notification.isTransient = !!hints['transient'];
if (hints['transient'])
notification.displayHint |= MessageTray.DisplayHint.TRANSIENT;
let privacyScope = hints['x-gnome-privacy-scope'] || 'user';
notification.privacyScope = privacyScope === 'system'
? MessageTray.PrivacyScope.SYSTEM
: MessageTray.PrivacyScope.USER;
if (hints['x-gnome-privacy-scope'] === 'system')
notification.displayHint |= MessageTray.DisplayHint.SHOW_CONTENT_ON_LOCKSCREEN;
else
// Always show the notification on the lockscreen but without any content
notification.displayHint |= MessageTray.DisplayHint.SHOW_ON_LOCKSCREEN;
// Only fallback to 'app-icon' when the source doesn't have a valid app
const sourceGIcon = source.app ? null : this._iconForNotificationData(appIcon);
@ -357,18 +364,11 @@ class FdoNotificationDaemonSource extends MessageTray.Source {
this.notify('icon');
}
let tracker = Shell.WindowTracker.get_default();
// Acknowledge notifications that are resident and their app has the
// current focus so that we don't show a banner.
if (notification.resident && this.app && tracker.focus_app === this.app)
notification.acknowledged = true;
this.addNotification(notification);
}
open() {
this.openApp();
this.destroyNonResidentNotifications();
}
openApp() {
@ -409,31 +409,41 @@ const GtkNotificationDaemonNotification = GObject.registerClass(
class GtkNotificationDaemonNotification extends MessageTray.Notification {
constructor(source, id, notification) {
super({source});
this._serialized = GLib.Variant.new('a{sv}', notification);
this.id = id;
this.id = id;
this.update(notification);
}
update(notification) {
const {
title,
body,
'markup-body': markupBody,
icon: gicon,
urgent,
priority,
buttons,
sound,
'default-action': defaultAction,
'default-action-target': defaultActionTarget,
'display-hint': displayHint,
timestamp: time,
} = notification;
if (priority) {
let urgency = PRIORITY_URGENCY_MAP[priority.unpack()];
this.urgency = urgency !== undefined ? urgency : MessageTray.Urgency.NORMAL;
} else if (urgent) {
this.urgency = urgent.unpack()
? MessageTray.Urgency.CRITICAL
: MessageTray.Urgency.NORMAL;
} else {
this.urgency = MessageTray.Urgency.NORMAL;
}
const urgency = PRIORITY_URGENCY_MAP[priority?.unpack()] ??
urgent?.unpack() ? MessageTray.Urgency.CRITICAL : MessageTray.Urgency.NORMAL;
this.set({
title: title.unpack(),
body: markupBody?.unpack() ?? body?.unpack() ?? null,
useBodyMarkup: !!markupBody,
gicon: gicon
? Gio.icon_deserialize(gicon) : null,
sound: sound ? MessageTray.Sound.deserialize(sound) : null,
datetime: time
? GLib.DateTime.new_from_unix_local(time.unpack()) : null,
urgency,
});
if (buttons) {
buttons.deepUnpack().forEach(button => {
@ -443,37 +453,55 @@ class GtkNotificationDaemonNotification extends MessageTray.Notification {
});
}
let showOnLockscreen = true;
let showContentOnLockscreen = true;
if (displayHint) {
for (const hint of displayHint.deepUnpack()) {
if (hint === 'transient')
this.displayHint |= MessageTray.DisplayHint.TRANSIENT;
else if (hint === 'tray')
this.displayHint |= MessageTray.DisplayHint.NO_BANNER;
else if (hint === 'persistent')
this.displayHint |= MessageTray.DisplayHint.PERSISTENT;
else if (hint === 'resident')
this.displayHint |= MessageTray.DisplayHint.RESIDENT;
else if (hint === 'hide-on-lockscreen')
showOnLockscreen = false;
else if (hint === 'hide-content-on-lockscreen')
showContentOnLockscreen = false;
}
}
if (showOnLockscreen) {
if (showContentOnLockscreen)
this.displayHint |= MessageTray.DisplayHint.SHOW_CONTENT_ON_LOCKSCREEN;
else
this.displayHint |= MessageTray.DisplayHint.SHOW_ON_LOCKSCREEN;
}
this._serialized = GLib.Variant.new('a{sv}', notification);
this._defaultAction = defaultAction?.unpack();
this._defaultActionTarget = defaultActionTarget;
this.set({
title: title.unpack(),
body: body?.unpack(),
gicon: gicon
? Gio.icon_deserialize(gicon) : null,
datetime: time
? GLib.DateTime.new_from_unix_local(time.unpack()) : null,
});
}
_activateAction(namespacedActionId, target) {
if (namespacedActionId) {
if (namespacedActionId.startsWith('app.')) {
let actionId = namespacedActionId.slice('app.'.length);
this.source.activateAction(actionId, target);
}
} else {
this.source.open();
}
_activateAction(actionId, target) {
if (actionId.startsWith('app.'))
this.source.activateAction(actionId.slice('app.'.length), target);
else
this.source.emitActionInvoked(this.id, actionId, target);
}
_onButtonClicked(button) {
let {action, target} = button;
const {action, target} = button;
this._activateAction(action.unpack(), target);
}
activate() {
this._activateAction(this._defaultAction, this._defaultActionTarget);
if (this._defaultAction)
this._activateAction(this._defaultAction, this._defaultActionTarget);
else
this.source.open();
super.activate();
}
@ -486,7 +514,7 @@ function InvalidAppError() {}
export const GtkNotificationDaemonAppSource = GObject.registerClass(
class GtkNotificationDaemonAppSource extends MessageTray.Source {
constructor(appId) {
constructor(appId, dbusImpl) {
if (!Gio.Application.id_is_valid(appId))
throw new InvalidAppError();
@ -502,6 +530,7 @@ class GtkNotificationDaemonAppSource extends MessageTray.Source {
this._appId = appId;
this._app = app;
this._dbusImpl = dbusImpl;
this._notifications = {};
this._notificationPending = false;
@ -517,6 +546,22 @@ class GtkNotificationDaemonAppSource extends MessageTray.Source {
Main.panel.closeCalendar();
}
emitActionInvoked(notificationId, actionId, target) {
const context = global.create_app_launch_context(0, -1);
const info = this._app.get_app_info();
const token = context.get_startup_notify_id(info, []);
this._dbusImpl.emit_signal('ActionInvoked',
GLib.Variant.new('(sssava{sv})', [
this._appId,
notificationId,
actionId,
target ? [target] : [],
{'activation-token': GLib.Variant.new_string(token)},
])
);
}
open() {
this._app.activate();
Main.overview.hide();
@ -566,11 +611,11 @@ class GtkNotificationDaemon {
constructor() {
this._sources = {};
this._loadNotifications();
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GtkNotificationsIface, this);
this._dbusImpl.export(Gio.DBus.session, '/org/gtk/Notifications');
this._loadNotifications();
Gio.DBus.session.own_name('org.gtk.Notifications', Gio.BusNameOwnerFlags.REPLACE, null, null);
}
@ -578,7 +623,7 @@ class GtkNotificationDaemon {
if (this._sources[appId])
return this._sources[appId];
let source = new GtkNotificationDaemonAppSource(appId);
const source = new GtkNotificationDaemonAppSource(appId, this._dbusImpl);
source.connect('destroy', () => {
delete this._sources[appId];
@ -656,13 +701,22 @@ class GtkNotificationDaemon {
throw e;
}
let timestamp = GLib.DateTime.new_now_local().to_unix();
const timestamp = GLib.DateTime.new_now_local().to_unix();
notificationSerialized['timestamp'] = new GLib.Variant('x', timestamp);
const showAsNew = notificationSerialized['display-hint']?.deepUnpack().some(hint => hint === 'show-as-new');
const oldNotification = source._notifications[notificationId];
const notification = new GtkNotificationDaemonNotification(source,
notificationId,
notificationSerialized);
source.addNotification(notification);
if (showAsNew || !oldNotification) {
const notification = new GtkNotificationDaemonNotification(
source,
notificationId,
notificationSerialized
);
source.addNotification(notification);
} else {
oldNotification.update(notificationSerialized);
}
invocation.return_value(null);
}

View file

@ -16,9 +16,7 @@ export const ANIMATION_TIME = 250;
import * as DND from './dnd.js';
import * as LayoutManager from './layout.js';
import * as Main from './main.js';
import * as MessageTray from './messageTray.js';
import * as OverviewControls from './overviewControls.js';
import * as Params from '../misc/params.js';
import * as SwipeTracker from './swipeTracker.js';
import * as WindowManager from './windowManager.js';
import * as WorkspaceThumbnail from './workspaceThumbnail.js';
@ -27,36 +25,6 @@ const DND_WINDOW_SWITCH_TIMEOUT = 750;
const OVERVIEW_ACTIVATION_TIMEOUT = 0.5;
class ShellInfo {
setMessage(title, options) {
options = Params.parse(options, {
undoCallback: null,
forFeedback: false,
});
const source = MessageTray.getSystemSource();
let undoCallback = options.undoCallback;
let forFeedback = options.forFeedback;
if (!this._notification) {
this._notification = new MessageTray.Notification({
source,
isTransient: true,
forFeedback,
});
this._notification.connect('destroy', () => delete this._notification);
}
this._notification.set({title});
this._notification.clearActions();
if (undoCallback)
this._notification.addAction(_('Undo'), () => undoCallback());
source.addNotification(this._notification);
}
}
const OverviewActor = GObject.registerClass(
class OverviewActor extends St.BoxLayout {
_init() {
@ -252,8 +220,6 @@ export class Overview extends Signals.EventEmitter {
this._overview._delegate = this;
Main.layoutManager.overviewGroup.add_child(this._overview);
this._shellInfo = new ShellInfo();
Main.layoutManager.connect('monitors-changed', this._relayout.bind(this));
this._relayout();
@ -275,18 +241,6 @@ export class Overview extends Signals.EventEmitter {
this._swipeTracker = swipeTracker;
}
//
// options:
// - undoCallback (function): the callback to be called if undo support is needed
// - forFeedback (boolean): whether the message is for direct feedback of a user action
//
setMessage(text, options) {
if (this.isDummy)
return;
this._shellInfo.setMessage(text, options);
}
_changeShownState(state) {
const {allowedTransitions} =
OVERVIEW_SHOWN_TRANSITIONS[this._shownState];

View file

@ -2092,7 +2092,7 @@ export const ScreenshotUI = GObject.registerClass({
title,
// Translators: notification body when a screencast was recorded.
body: this._screencastPath ? _('Click here to view the video.') : '',
isTransient: true,
displayHint: MessageTray.DisplayHint.TRANSIENT,
});
if (this._screencastPath) {
@ -2337,7 +2337,7 @@ function _storeScreenshot(bytes, pixbuf) {
body: _('You can paste the image from the clipboard.'),
datetime: time,
gicon: content,
isTransient: true,
displayHint: MessageTray.DisplayHint.TRANSIENT,
});
if (!disableSaveToDisk) {

View file

@ -199,7 +199,7 @@ export class ShellMountOperation {
source,
title,
body,
isTransient: true,
displayHint: MessageTray.DisplayHint.TRANSIENT,
iconName: 'media-removable-symbolic',
});

View file

@ -2036,7 +2036,7 @@ class Indicator extends SystemIndicator {
body: _('Activation of network connection failed'),
iconName: 'network-error-symbolic',
urgency: MessageTray.Urgency.HIGH,
isTransient: true,
displayHint: MessageTray.DisplayHint.TRANSIENT,
});
this._notification.connect('destroy',
() => (this._notification = null));

View file

@ -166,7 +166,9 @@ const NotificationsBox = GObject.registerClass({
_shouldShowDetails(source) {
return source.policy.detailsInLockScreen ||
source.narrowestPrivacyScope === MessageTray.PrivacyScope.SYSTEM;
source.notifications.every(n =>
n.displayHint & MessageTray.DisplayHint.SHOW_CONTENT_ON_LOCKSCREEN
);
}
_updateSourceBoxStyle(source, obj, box) {

View file

@ -43,7 +43,7 @@ export class WindowAttentionHandler {
source,
title,
body,
forFeedback: true,
displayHint: MessageTray.DisplayHint.FORCE_IMMEDIATELY,
});
notification.connect('activated', () => {
source.open();