From f03adeb418294ec37fad2eaad59c5e9f27682b7e Mon Sep 17 00:00:00 2001 From: fredcw Date: Thu, 12 Dec 2024 06:55:56 +0000 Subject: [PATCH] GWL: add notification badges GWL: add notification badges to top right of panel icons and add config option to disable notification badges Remove 'notifications' extension role as it only allows for one extension. Use extensionsHandlingNotifications variable in messageTray.js for applets to increment/decrement when added/removed from panel instead. Ensure notificationDaemon can identify source of notifications from flatpak apps. Increase max notifications per source from 10 to 20. --- data/theme/cinnamon-sass/_colors.scss | 2 + .../cinnamon-sass/widgets/_windowlist.scss | 13 +- .../appGroup.js | 112 +++++++++++++----- .../applet.js | 62 +++++++++- .../settings-schema.json | 14 ++- .../workspace.js | 2 +- .../notifications@cinnamon.org/applet.js | 7 ++ .../notifications@cinnamon.org/metadata.json | 1 - js/ui/appletManager.js | 1 - js/ui/extension.js | 8 +- js/ui/messageTray.js | 11 +- js/ui/notificationDaemon.js | 67 +++++++++-- 12 files changed, 237 insertions(+), 63 deletions(-) diff --git a/data/theme/cinnamon-sass/_colors.scss b/data/theme/cinnamon-sass/_colors.scss index c9f18d40d4..b0b1063f83 100644 --- a/data/theme/cinnamon-sass/_colors.scss +++ b/data/theme/cinnamon-sass/_colors.scss @@ -18,6 +18,8 @@ $destructive_color: #ff7b63; $warning_color: #f8e45c; $warning_bg_color: #cd9309; +$notification_badge_bg_color: #e74b37; + $accent_color: #78aeed; $accent_bg_color: #3584e4; diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index a23da85c5c..8a0e8494d9 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -89,7 +89,7 @@ &-button-label { padding-left: 4px;} &-number-label { - font-size: 0.8em; + font-size: 10px; z-index: 99; } @@ -97,6 +97,17 @@ border-radius: 9999px; background-color: $bg_color; } + + &-notifications-badge { + border-radius: 9999px; + background-color: $notification_badge_bg_color; + color: $fg_color; + font-size: 13px; + } + + &-notifications-badge-label { + z-index: 99; + } } // classic window list diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js index 6a0f0990c5..0594af49de 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js @@ -119,31 +119,50 @@ class AppGroup { }); this.actor.add_child(this.progressOverlay); - // Create the app button icon, number label, and text label for titleDisplay + // Create the app button icon, window count and notification badges, and text label for titleDisplay this.iconBox = new Cinnamon.Slicer({name: 'appMenuIcon'}); this.actor.add_child(this.iconBox); this.setActorAttributes(null, params.metaWindow); - this.badge = new St.BoxLayout({ + this.windowsBadge = new St.BoxLayout({ style_class: 'grouped-window-list-badge', important: true, - x_align: St.Align.START, + x_align: St.Align.MIDDLE, y_align: St.Align.MIDDLE, show_on_set_parent: false, }); - this.numberLabel = new St.Label({ + this.windowsBadgeLabel = new St.Label({ style_class: 'grouped-window-list-number-label', important: true, - text: '', - anchor_x: -3 * global.ui_scale, + text: '' + }); + this.windowsBadgeLabel.clutter_text.ellipsize = false; + this.windowsBadge.add(this.windowsBadgeLabel, { + x_align: St.Align.START, + y_align: St.Align.START, + }); + this.actor.add_child(this.windowsBadge); + this.windowsBadge.set_text_direction(St.TextDirection.LTR); + + this.notificationsBadge = new St.BoxLayout({ + style_class: 'grouped-window-list-notifications-badge', + important: true, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE, + show_on_set_parent: false, }); - this.numberLabel.clutter_text.ellipsize = false; - this.badge.add(this.numberLabel, { + this.notificationsBadgeLabel = new St.Label({ + style_class: 'grouped-window-list-notifications-badge-label', + important: true, + text: '' + }); + this.notificationsBadgeLabel.clutter_text.ellipsize = false; + this.notificationsBadge.add(this.notificationsBadgeLabel, { x_align: St.Align.START, y_align: St.Align.START, }); - this.actor.add_child(this.badge); - this.badge.set_text_direction(St.TextDirection.LTR); + this.actor.add_child(this.notificationsBadge); + this.notificationsBadge.set_text_direction(St.TextDirection.LTR); this.label = new St.Label({ style_class: 'grouped-window-list-button-label', @@ -372,6 +391,8 @@ class AppGroup { const allocWidth = box.x2 - box.x1; const allocHeight = box.y2 - box.y1; const childBox = new Clutter.ActorBox(); + const windowBadgeBox = new Clutter.ActorBox(); + const notifBadgeBox = new Clutter.ActorBox(); const direction = this.actor.get_text_direction(); // Set the icon to be left-justified (or right-justified) and centered vertically @@ -394,15 +415,37 @@ class AppGroup { this.iconBox.allocate(childBox, flags); - // Set badge position - const windowCountFactor = this.groupState.windowCount > 9 ? 1.5 : 2; - const badgeOffset = 2 * global.ui_scale; - childBox.x1 = childBox.x1 - badgeOffset; - childBox.x2 = childBox.x1 + (this.numberLabel.width * windowCountFactor); - childBox.y1 = Math.max(childBox.y1 - badgeOffset, 0); - childBox.y2 = childBox.y1 + this.badge.get_preferred_height(childBox.get_width())[1]; - - this.badge.allocate(childBox, flags); + // Set windows badge position + const windowBadgeOffset = 3 * global.ui_scale; + const windowBadgeXCenter = this.iconBox.x + windowBadgeOffset; + const windowBadgeYCenter = this.iconBox.y + windowBadgeOffset; + const [wLabelMinWidth, wLabelMinHeight, wLabelNaturalWidth, wLabelNaturalHeight] = this.windowsBadgeLabel.get_preferred_size(); + const windowBadgesize = Math.max(wLabelNaturalWidth, wLabelNaturalHeight); + windowBadgeBox.x1 = Math.max(windowBadgeXCenter - Math.floor(windowBadgesize / 2), 0); + windowBadgeBox.x2 = windowBadgeBox.x1 + windowBadgesize; + windowBadgeBox.y1 = Math.max(windowBadgeYCenter - Math.floor(windowBadgesize / 2), 0); + windowBadgeBox.y2 = windowBadgeBox.y1 + windowBadgesize; + const windowLabelPosX = Math.floor((windowBadgesize - wLabelNaturalWidth) / 2); + const windowLabelPosY = Math.floor((windowBadgesize - wLabelNaturalHeight) / 2); + this.windowsBadgeLabel.set_anchor_point(-windowLabelPosX, -windowLabelPosY); + this.windowsBadge.set_size(windowBadgesize, windowBadgesize); + this.windowsBadge.allocate(windowBadgeBox, flags); + + // Set notifications badge position + const notifBadgeOffset = 3 * global.ui_scale; + const notifBadgeXCenter = this.iconBox.x + this.iconBox.width - notifBadgeOffset; + const notifBadgeYCenter = this.iconBox.y + notifBadgeOffset; + const [nLabelMinWidth, nLabelMinHeight, nLabelNaturalWidth, nLabelNaturalHeight] = this.notificationsBadgeLabel.get_preferred_size(); + const notifBadgesize = Math.max(nLabelNaturalWidth, nLabelNaturalHeight); + notifBadgeBox.x2 = Math.min(notifBadgeXCenter + Math.floor(notifBadgesize / 2), box.x2); + notifBadgeBox.x1 = notifBadgeBox.x2 - notifBadgesize; + notifBadgeBox.y1 = Math.max(notifBadgeYCenter - Math.floor(notifBadgesize / 2), 0); + notifBadgeBox.y2 = notifBadgeBox.y1 + notifBadgesize; + const notifLabelPosX = Math.floor((notifBadgesize - nLabelNaturalWidth) / 2); + const notifLabelPosY = Math.floor((notifBadgesize - nLabelNaturalHeight) / 2); + this.notificationsBadgeLabel.set_anchor_point(-notifLabelPosX, -notifLabelPosY); + this.notificationsBadge.set_size(notifBadgesize, notifBadgesize); + this.notificationsBadge.allocate(notifBadgeBox, flags); // Set label position if (this.drawLabel) { @@ -676,8 +719,8 @@ class AppGroup { } showOrderLabel(number) { - this.numberLabel.text = (number + 1).toString(); - this.badge.show(); + this.windowsBadgeLabel.text = (number + 1).toString(); + this.windowsBadge.show(); } launchNewInstance(offload=false) { @@ -917,6 +960,7 @@ class AppGroup { this.setIcon(metaWindow) this.calcWindowNumber(); + this.updateNotificationsBadge(); this.onFocusChange(); } set({ @@ -1074,20 +1118,24 @@ class AppGroup { calcWindowNumber() { if (this.groupState.willUnmount) return; - const windowCount = this.groupState.metaWindows ? this.groupState.metaWindows.length : 0; - this.numberLabel.text = windowCount.toString(); - - this.groupState.set({windowCount}); + this.groupState.set({windowCount: this.groupState.metaWindows ? this.groupState.metaWindows.length : 0}); + + if (this.groupState.windowCount > 1 && this.state.settings.enableWindowCountBadges) { + this.windowsBadgeLabel.text = this.groupState.windowCount.toString(); + this.windowsBadge.show(); + } else { + this.windowsBadge.hide(); + } + } - if (this.state.settings.numDisplay) { - if (windowCount <= 1) { - this.badge.hide(); - } else { - this.badge.show(); + updateNotificationsBadge() { + const nCount = Main.notificationDaemon.getNotificationCountForApp(this.groupState.app); - } + if (nCount > 0 && this.state.settings.enableNotificationBadges) { + this.notificationsBadgeLabel.text = nCount.toString(); + this.notificationsBadge.show(); } else { - this.badge.hide(); + this.notificationsBadge.hide(); } } diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 75d1c85208..9be3d5c727 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -9,6 +9,7 @@ const Applet = imports.ui.applet; const Cinnamon = imports.gi.Cinnamon; const Main = imports.ui.main; const DND = imports.ui.dnd; +const MessageTray = imports.ui.messageTray; const {AppletSettings} = imports.ui.settings; const {SignalManager} = imports.misc.signalManager; const {throttle, unref, trySpawnCommandLine} = imports.misc.util; @@ -254,7 +255,7 @@ class GroupedWindowListApplet extends Applet.Applet { cycleWindows: (e, source) => this.handleScroll(e, source), openAbout: () => this.openAbout(), configureApplet: () => this.configureApplet(), - removeApplet: (event) => this.confirmRemoveApplet(event), + removeApplet: (event) => this.confirmRemoveApplet(event) }); this.settings = new AppletSettings(this.state.settings, metadata.uuid, instance_id); @@ -291,6 +292,7 @@ class GroupedWindowListApplet extends Applet.Applet { this.signals.connect(global.display, 'window-created', (...args) => this.onWindowCreated(...args)); this.signals.connect(global.settings, 'changed::panel-edit-mode', (...args) => this.on_panel_edit_mode_changed(...args)); this.signals.connect(Main.themeManager, 'theme-set', (...args) => this.refreshCurrentWorkspace(...args)); + this.signals.connect(Main.messageTray, 'notify-applet-update', this._onNotificationReceived.bind(this)); } bindSettings() { @@ -307,7 +309,8 @@ class GroupedWindowListApplet extends Applet.Applet { {key: 'super-num-hotkeys', value: 'SuperNumHotkeys', cb: this.bindAppKeys}, {key: 'title-display', value: 'titleDisplay', cb: this.updateTitleDisplay}, {key: 'launcher-animation-effect', value: 'launcherAnimationEffect', cb: null}, - {key: 'number-display', value: 'numDisplay', cb: this.updateWindowNumberState}, + {key: 'enable-window-count-badges', value: 'enableWindowCountBadges', cb: this.onEnableWindowCountBadgeChange}, + {key: 'enable-notification-badges', value: 'enableNotificationBadges', cb: this.onEnableNotificationsChange}, {key: 'enable-app-button-dragging', value: 'enableDragging', cb: this.draggableSettingChanged}, {key: 'thumbnail-scroll-behavior', value: 'thumbnailScrollBehavior', cb: null}, {key: 'show-thumbnails', value: 'showThumbs', cb: this.updateVerticalThumbnailState}, @@ -357,6 +360,7 @@ class GroupedWindowListApplet extends Applet.Applet { } this.bindAppKeys(); this.state.set({appletReady: true}); + MessageTray.extensionsHandlingNotifications++; } _updateState(initialUpdate) { @@ -424,6 +428,7 @@ class GroupedWindowListApplet extends Applet.Applet { }); this.settings.finalize(); unref(this, RESERVE_KEYS); + MessageTray.extensionsHandlingNotifications--; } on_panel_icon_size_changed(iconSize) { @@ -584,7 +589,7 @@ class GroupedWindowListApplet extends Applet.Applet { }); } - updateWindowNumberState() { + onEnableWindowCountBadgeChange() { this.workspaces.forEach( workspace => workspace.calcAllWindowNumbers() ); @@ -1022,6 +1027,57 @@ class GroupedWindowListApplet extends Applet.Applet { this.state.set({thumbnailCloseButtonOffset: global.ui_scale > 1 ? -10 : 0}); this.refreshAllWorkspaces(); } + + _onNotificationReceived(mtray, notification) { + let appId = notification.source.app?.get_id(); + + if (!appId) { + return; + } + + // Add notification to all appgroups with appId. + let notificationAdded = false; + + this.workspaces.forEach(workspace => { + if (!workspace) return; + workspace.appGroups.forEach(appGroup => { + if (!appGroup || !appGroup.groupState || appGroup.groupState.willUnmount) return; + if (appId === appGroup.groupState.appId) { + notificationAdded = true; + appGroup.updateNotificationsBadge(); + } + }); + }); + + if (notificationAdded) { + notification.appId = appId; + notification.connect('destroy', () => this._onNotificationDestroyed(notification)); + } + } + + _onNotificationDestroyed(notification) { + if (!this.workspaces) return; + + this.workspaces.forEach(workspace => { + if (!workspace) return; + workspace.appGroups.forEach(appGroup => { + if (!appGroup || !appGroup.groupState || appGroup.groupState.willUnmount) return; + if (notification.appId === appGroup.groupState.appId) { + appGroup.updateNotificationsBadge(); + } + }); + }); + } + + onEnableNotificationsChange() { + this.workspaces.forEach(workspace => { + if (!workspace) return; + workspace.appGroups.forEach(appGroup => { + if (!appGroup || !appGroup.groupState || appGroup.groupState.willUnmount) return; + appGroup.updateNotificationsBadge(); + }); + }); + } } function main(metadata, orientation, panel_height, instance_id) { diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json index b86af45073..c4f1b49cc2 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json @@ -50,7 +50,8 @@ "keys": [ "title-display", "launcher-animation-effect", - "number-display", + "enable-window-count-badges", + "enable-notification-badges", "enable-app-button-dragging" ] }, @@ -184,10 +185,17 @@ "Scale": 3 } }, - "number-display": { + "enable-window-count-badges": { "type": "checkbox", "default": true, - "description": "Show window count numbers" + "description": "Show window count badges", + "tooltip": "Indicate on the panel the number of open windows an application has" + }, + "enable-notification-badges": { + "type": "checkbox", + "default": true, + "description": "Show notification badges", + "tooltip": "Indicate on the panel when an application has notifications" }, "enable-app-button-dragging": { "type": "checkbox", diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 4329489297..4af591ee8c 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -333,7 +333,7 @@ class Workspace { calcAllWindowNumbers() { this.appGroups.forEach( appGroup => { - appGroup.calcWindowNumber(appGroup.groupState.metaWindows); + appGroup.calcWindowNumber(); }); } diff --git a/files/usr/share/cinnamon/applets/notifications@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/notifications@cinnamon.org/applet.js index 883b252a14..a0f45180d9 100644 --- a/files/usr/share/cinnamon/applets/notifications@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/notifications@cinnamon.org/applet.js @@ -7,6 +7,7 @@ const PopupMenu = imports.ui.popupMenu; const St = imports.gi.St; const Mainloop = imports.mainloop; const Urgency = imports.ui.messageTray.Urgency; +const MessageTray = imports.ui.messageTray; const NotificationDestroyedReason = imports.ui.messageTray.NotificationDestroyedReason; const Settings = imports.ui.settings; const Gettext = imports.gettext.domain("cinnamon-applets"); @@ -55,6 +56,11 @@ class CinnamonNotificationsApplet extends Applet.TextIconApplet { Main.keybindingManager.removeXletHotKey(this, "notification-open"); Main.keybindingManager.removeXletHotKey(this, "notification-clear"); global.settings.disconnect(this.panelEditModeHandler); + + MessageTray.extensionsHandlingNotifications--; + if (MessageTray.extensionsHandlingNotifications === 0) { + this._clear_all(); + } } _openMenu() { @@ -266,6 +272,7 @@ class CinnamonNotificationsApplet extends Applet.TextIconApplet { on_applet_added_to_panel() { this.on_orientation_changed(this._orientation); + MessageTray.extensionsHandlingNotifications++; } on_orientation_changed (orientation) { diff --git a/files/usr/share/cinnamon/applets/notifications@cinnamon.org/metadata.json b/files/usr/share/cinnamon/applets/notifications@cinnamon.org/metadata.json index 6daa046102..0d9d00af31 100644 --- a/files/usr/share/cinnamon/applets/notifications@cinnamon.org/metadata.json +++ b/files/usr/share/cinnamon/applets/notifications@cinnamon.org/metadata.json @@ -2,6 +2,5 @@ "uuid": "notifications@cinnamon.org", "name": "Notifications", "description": "Click to display and manage system notifications", -"role": "notifications", "icon": "cs-notifications" } diff --git a/js/ui/appletManager.js b/js/ui/appletManager.js index b8bfc679d1..3a2ecdfb7d 100644 --- a/js/ui/appletManager.js +++ b/js/ui/appletManager.js @@ -25,7 +25,6 @@ var appletsLoaded = false; // FIXME: This role stuff is checked in extension.js, why not move checks from here to there? var Roles = { - NOTIFICATIONS: 'notifications', PANEL_LAUNCHER: 'panellauncher', WINDOW_ATTENTION_HANDLER: 'windowattentionhandler', WINDOW_LIST: 'windowlist' diff --git a/js/ui/extension.js b/js/ui/extension.js index 8c9ea22dce..8ef64bb114 100644 --- a/js/ui/extension.js +++ b/js/ui/extension.js @@ -104,7 +104,6 @@ var Type = { }), APPLET: _createExtensionType("Applet", "applets", AppletManager, { roles: { - notifications: null, windowlist: null, windowattentionhandler: null, panellauncher: null, @@ -113,7 +112,6 @@ var Type = { }), DESKLET: _createExtensionType("Desklet", "desklets", DeskletManager, { roles: { - notifications: null, windowlist: null, windowattentionhandler: null } @@ -339,7 +337,7 @@ Extension.prototype = { // If a role is set, make sure it's a valid one let meta_role_list_str = this.meta['role']; if (meta_role_list_str) { - let meta_roles = meta_role_list_str.replace(" ", "").split(","); + let meta_roles = meta_role_list_str.replaceAll(" ", "").split(","); for (let role of meta_roles) { if (!(role in Type[this.upperType].roles)) { throw logError(`Unknown role definition: ${role} in metadata.json`, this.uuid); @@ -415,7 +413,7 @@ Extension.prototype = { lockRole: function(roleProvider) { if (this.meta && this.meta.role) { let meta_role_list_str = this.meta.role; - let meta_roles = meta_role_list_str.replace(" ", "").split(","); + let meta_roles = meta_role_list_str.replaceAll(" ", "").split(","); let avail_roles = []; @@ -448,7 +446,7 @@ Extension.prototype = { unlockRoles: function() { if (this.meta.role) { let meta_role_list_str = this.meta.role; - let meta_roles = meta_role_list_str.replace(" ", "").split(","); + let meta_roles = meta_role_list_str.replaceAll(" ", "").split(","); for (let role of meta_roles) { if (Type[this.upperType].roles[role] === this.uuid) { diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index 63e46fa2af..7d224852c4 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -30,6 +30,11 @@ var LONGER_HIDE_TIMEOUT = 0.6; const NOTIFICATION_IMAGE_SIZE = 125; const NOTIFICATION_IMAGE_OPACITY = 230; // 0 - 255 +// Applets wishing to receive the "notify-applet-update" signal should increment and decrement this value when +// added and removed from the panel respectfully as when this value is zero, the signal will not be emitted and +// notifications will be automatically destroyed after being shown. +var extensionsHandlingNotifications = 0; + var State = Object.freeze({ HIDDEN: 0, SHOWING: 1, @@ -634,7 +639,7 @@ function Source(title) { Source.prototype = { ICON_SIZE: 24, - MAX_NOTIFICATIONS: 10, + MAX_NOTIFICATIONS: 20, _init: function (title) { this.title = title; @@ -1017,7 +1022,7 @@ MessageTray.prototype = { if (this._notification.urgency != Urgency.CRITICAL) { this._updateNotificationTimeout(this.notificationDuration * 1000); - } else if (AppletManager.get_role_provider_exists(AppletManager.Roles.NOTIFICATIONS)) { + } else if (extensionsHandlingNotifications > 0) { this._updateNotificationTimeout(NOTIFICATION_CRITICAL_TIMEOUT_WITH_APPLET * 1000); } }, @@ -1076,7 +1081,7 @@ MessageTray.prototype = { this._notificationBin.hide(); this._notificationBin.child = null; let notification = this._notification; - if (AppletManager.get_role_provider_exists(AppletManager.Roles.NOTIFICATIONS) && !this._notificationRemoved) { + if (extensionsHandlingNotifications > 0 && !this._notificationRemoved) { this.emit('notify-applet-update', notification); } else { if (notification.isTransient) diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js index f14f844ab3..7c01633a5c 100644 --- a/js/ui/notificationDaemon.js +++ b/js/ui/notificationDaemon.js @@ -13,10 +13,6 @@ const MessageTray = imports.ui.messageTray; const Params = imports.misc.params; const Mainloop = imports.mainloop; -// don't automatically clear these apps' notifications on window focus -// lowercase only -const AUTOCLEAR_BLACKLIST = ['chromium', 'firefox', 'google chrome']; - let nextNotificationId = 1; // Should really be defined in Gio.js @@ -231,7 +227,8 @@ NotificationDaemon.prototype = { } } - let source = new Source(title, pid, sender, trayIcon); + const desktopEntryHint = ndata && ndata.hints['desktop-entry']; + let source = new Source(title, pid, sender, desktopEntryHint, trayIcon); source.setTransient(isForTransientNotification); if (!isForTransientNotification) { @@ -405,7 +402,7 @@ NotificationDaemon.prototype = { } let [pid] = result; - source = this._getSource(appName, pid, ndata, sender); + source = this._getSource(appName, pid, ndata, sender, null); // We only store sender-pid entries for persistent sources. // Removing the entries once the source is destroyed @@ -586,8 +583,6 @@ NotificationDaemon.prototype = { return; let name = tracker.focus_app.get_name(); - if (name && AUTOCLEAR_BLACKLIST.includes(name.toLowerCase())) - return; for (let i = 0; i < this._sources.length; i++) { let source = this._sources[i]; @@ -616,20 +611,31 @@ NotificationDaemon.prototype = { let source = this._lookupSource(null, icon.pid, true); if (source) source.destroy(); + }, + + getNotificationCountForApp(app) { + const foundSource = this._sources.find(source => source.app === app); + + if (foundSource) { + return foundSource.notifications.length; + } else { + return 0; + } } }; -function Source(title, pid, sender, trayIcon) { - this._init(title, pid, sender, trayIcon); +function Source(title, pid, sender, desktopEntryHint, trayIcon) { + this._init(title, pid, sender, desktopEntryHint, trayIcon); } Source.prototype = { __proto__: MessageTray.Source.prototype, - _init: function(title, pid, sender, trayIcon) { + _init: function(title, pid, sender, desktopEntryHint, trayIcon) { MessageTray.Source.prototype._init.call(this, title); this.initialTitle = title; + this.desktopEntryHint = desktopEntryHint; this.pid = pid; if (sender) @@ -676,8 +682,25 @@ Source.prototype = { let app; app = Cinnamon.WindowTracker.get_default().get_app_from_pid(this.pid); - if (app != null) - return app; + + // With flatpak apps, the notification's pid is that of the portal so use the desktop-entry hint instead. + if (!app && this.desktopEntryHint) { + const exceptions = { + "vivaldi-stable": "com.vivaldi.Vivaldi", + "brave-browser": "com.brave.Browser", + "google-chrome": "com.google.Chrome", + "microsoft-edge": "com.microsoft.Edge", + "opera": "com.opera.Opera" + }; + const exception = exceptions[this.desktopEntryHint]; + app = Cinnamon.AppSystem.get_default().lookup_flatpak_app_id(exception ? exception : this.desktopEntryHint); + if (!app) { + app = this._findUniqueAppByName(this.initialTitle); + } + if (!app) log('Failed to find flatpak app for notification with desktop-entry hint:', this.desktopEntryHint); + } + + if (app) return app; if (this.trayIcon) { app = Cinnamon.AppSystem.get_default().lookup_wmclass(this.trayIcon.wmclass); @@ -688,6 +711,24 @@ Source.prototype = { return null; }, + _findUniqueAppByName(appName) { + const appSystem = Cinnamon.AppSystem.get_default(); + const runningApps = appSystem.get_running(); + const matches = []; + + for (const app of runningApps) { + if (app.get_name() === appName) { + matches.push(app); + } + } + + if (matches.length === 1) { + return matches[0]; + } else { + return null; + } + }, + _setApp: function() { if (this.app) return;