From 90fcca8af48b8fbcba5cb95b1459d60eaf34356d Mon Sep 17 00:00:00 2001 From: lenemter Date: Mon, 6 Oct 2025 00:59:32 +0300 Subject: [PATCH 1/4] Add Notification Portal v2 backend --- meson.build | 1 + src/AbstractBubble.vala | 15 ++++-- src/Application.vala | 18 ++++++- src/Bubble.vala | 2 +- src/DBus.vala | 13 ++--- src/FdoActionGroup.vala | 2 +- src/PortalProxy.vala | 117 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 src/PortalProxy.vala diff --git a/meson.build b/meson.build index 41b6299e..a38ca49f 100644 --- a/meson.build +++ b/meson.build @@ -27,6 +27,7 @@ executable( 'src/DBus.vala', 'src/FdoActionGroup.vala', 'src/Notification.vala', + 'src/PortalProxy.vala', 'src/Widgets/MaskedImage.vala', css_gresource, dependencies: [ diff --git a/src/AbstractBubble.vala b/src/AbstractBubble.vala index f0076bc9..2092940e 100644 --- a/src/AbstractBubble.vala +++ b/src/AbstractBubble.vala @@ -19,7 +19,13 @@ */ public class Notifications.AbstractBubble : Gtk.Window { - public signal void closed (uint32 reason) { + public enum CloseReason { + EXPIRED, + DISMISSED, + UNDEFINED + } + + public signal void closed (CloseReason reason) { close (); } @@ -94,10 +100,10 @@ public class Notifications.AbstractBubble : Gtk.Window { carousel.page_changed.connect ((index) => { if (index == 0) { - closed (Notifications.Server.CloseReason.DISMISSED); + closed (DISMISSED); } }); - close_button.clicked.connect (() => closed (Notifications.Server.CloseReason.DISMISSED)); + close_button.clicked.connect (() => closed (DISMISSED)); var motion_controller = new Gtk.EventControllerMotion (); motion_controller.enter.connect (pointer_enter); @@ -170,11 +176,10 @@ public class Notifications.AbstractBubble : Gtk.Window { } private bool timeout_expired () { - closed (Notifications.Server.CloseReason.EXPIRED); + closed (EXPIRED); return Source.REMOVE; } - private void get_blur_margins (out int left, out int right) { var width = get_width (); var distance = (1 - current_swipe_progress) * width; diff --git a/src/Application.vala b/src/Application.vala index 9685a898..3a623ffa 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -1,5 +1,5 @@ /* -* Copyright 2019-2023 elementary, Inc. (https://elementary.io) +* Copyright 2019-2025 elementary, Inc. (https://elementary.io) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public @@ -19,6 +19,8 @@ */ public class Notifications.Application : Gtk.Application { + public static Settings settings = new Settings ("io.elementary.notifications"); + private static Granite.Settings granite_settings; private static Gtk.Settings gtk_settings; @@ -32,6 +34,7 @@ public class Notifications.Application : Gtk.Application { protected override bool dbus_register (DBusConnection connection, string object_path) throws Error { try { new Notifications.Server (connection); + new Notifications.PortalProxy (connection); } catch (Error e) { Error.prefix_literal (out e, "Registring notification server failed: "); throw e; @@ -72,7 +75,18 @@ public class Notifications.Application : Gtk.Application { dbus_flags, () => hold (), (conn, name) => { - critical ("Could not aquire bus: %s", name); + critical ("Could not acquire bus: %s", name); + name_lost (); + } + ); + + Bus.own_name_on_connection ( + get_dbus_connection (), + "io.elementary.notifications.PortalProxy", + dbus_flags, + () => hold (), + (conn, name) => { + critical ("Could not acquire bus: %s", name); name_lost (); } ); diff --git a/src/Bubble.vala b/src/Bubble.vala index 4570f6ba..4e1f2cde 100644 --- a/src/Bubble.vala +++ b/src/Bubble.vala @@ -56,7 +56,7 @@ public class Notifications.Bubble : AbstractBubble { notification.app_info.launch_uris_async.begin (null, null, null, (obj, res) => { try { ((AppInfo) obj).launch_uris_async.end (res); - closed (Server.CloseReason.UNDEFINED); + closed (UNDEFINED); } catch (Error e) { warning ("Unable to launch app: %s", e.message); } diff --git a/src/DBus.vala b/src/DBus.vala index e0d0a540..2f88c88e 100644 --- a/src/DBus.vala +++ b/src/DBus.vala @@ -5,7 +5,7 @@ [DBus (name = "org.freedesktop.Notifications")] public class Notifications.Server : Object { - public enum CloseReason { + public enum FdoCloseReason { EXPIRED = 1, DISMISSED = 2, CLOSE_NOTIFICATION_CALL = 3, @@ -25,13 +25,10 @@ public class Notifications.Server : Object { private Gee.Map bubbles; private Confirmation? confirmation; - private Settings settings; - private uint action_group_id; private uint server_id; public Server (DBusConnection connection) throws Error { - settings = new Settings ("io.elementary.notifications"); bubbles = new Gee.HashMap (); action_group = new Fdo.ActionGroup (this); @@ -66,7 +63,7 @@ public class Notifications.Server : Object { throw new DBusError.FAILED (""); } - notification_closed (id, CloseReason.CLOSE_NOTIFICATION_CALL); + notification_closed (id, FdoCloseReason.CLOSE_NOTIFICATION_CALL); } public string [] get_capabilities () throws DBusError, IOError { @@ -144,10 +141,10 @@ public class Notifications.Server : Object { notification.buttons.add ({ label, action_name }); } - if (!settings.get_boolean ("do-not-disturb") || notification.priority == GLib.NotificationPriority.URGENT) { + if (!Application.settings.get_boolean ("do-not-disturb") || notification.priority == GLib.NotificationPriority.URGENT) { var app_settings = new Settings.with_path ( "io.elementary.notifications.applications", - settings.path.concat ("applications", "/", notification.app_id, "/") + Application.settings.path.concat ("applications", "/", notification.app_id, "/") ); if (app_settings.get_boolean ("bubbles")) { @@ -161,7 +158,7 @@ public class Notifications.Server : Object { return Gdk.EVENT_PROPAGATE; }); bubbles[id].closed.connect ((res) => { - if (res == CloseReason.EXPIRED && app_settings.get_boolean ("remember")) { + if (res == EXPIRED && app_settings.get_boolean ("remember")) { return; } diff --git a/src/FdoActionGroup.vala b/src/FdoActionGroup.vala index 29af9ec7..767fe8cd 100644 --- a/src/FdoActionGroup.vala +++ b/src/FdoActionGroup.vala @@ -95,7 +95,7 @@ public sealed class Notifications.Fdo.ActionGroup : Object, GLib.ActionGroup { uint32 id; while (iter.next ("u", out id)) { - server.notification_closed (id, Server.CloseReason.DISMISSED); + server.notification_closed (id, Server.FdoCloseReason.DISMISSED); } return; diff --git a/src/PortalProxy.vala b/src/PortalProxy.vala new file mode 100644 index 00000000..5439dc80 --- /dev/null +++ b/src/PortalProxy.vala @@ -0,0 +1,117 @@ +// Copyright + + +// TEST CALL FOR DSPY: +// 'io.elementary.mail.desktop' +// 'new-mail' +// {'title': <'New mail from John Doe'>, 'body': <'You have a new mail from John Doe. Click to read it.'>, 'priority': <'high'>} + +[DBus (name = "io.elementary.notifications.PortalProxy")] +public class Notifications.PortalProxy : Object { + private const string ID_FORMAT = "%s:%s"; + + public signal void action_invoked (string app_id, string id, string action_name, Variant[] parameters); + + public HashTable supported_options { get; construct; } + public uint version { get; default = 2; } + + [DBus (visible = false)] + public DBusConnection connection { private get; construct; } + + private uint server_id; + private Gee.Map bubbles; + + public PortalProxy (DBusConnection connection) { + Object (connection: connection); + } + + ~PortalProxy () { + connection.unregister_object (server_id); + } + + construct { + try { + server_id = connection.register_object ("/io/elementary/notifications/PortalProxy", this); + } catch (Error e) { + critical (e.message); + } + + supported_options = new HashTable (str_hash, str_equal); + bubbles = new Gee.HashMap (); + } + + public void add_notification (string app_id, string id, HashTable data) throws Error { + if (!("title" in data)) { + throw new DBusError.FAILED ("Can't show notification without title"); + } + + unowned var title = data["title"].get_string (); + + unowned string body = ""; + if ("body-markup" in data) { + body = data["body-markup"].get_string (); + } else if ("body" in data) { + body = data["body"].get_string (); + } + + var app_icon = app_id; + var hints = new HashTable (str_hash, str_equal); + + var notification = new Notification (app_id, app_icon, title, body, hints) { + buttons = new GenericArray (0) + }; + + if (!Application.settings.get_boolean ("do-not-disturb") || notification.priority == GLib.NotificationPriority.URGENT) { + var app_settings = new Settings.with_path ( + "io.elementary.notifications.applications", + Application.settings.path.concat ("applications", "/", notification.app_id, "/") + ); + + if (app_settings.get_boolean ("bubbles")) { + var full_id = ID_FORMAT.printf (app_id, id); + + if (bubbles.has_key (full_id) && bubbles[full_id] != null) { + bubbles[full_id].notification = notification; + } else { + bubbles[full_id] = new Bubble (notification); + bubbles[full_id].close_request.connect (() => { + bubbles[full_id] = null; + return Gdk.EVENT_PROPAGATE; + }); + } + + bubbles[full_id].present (); + } + + if (app_settings.get_boolean ("sounds")) { + var sound = notification.priority != URGENT ? "dialog-information" : "dialog-warning"; + + send_sound (sound); + } + } + } + + public void remove_notification (string app_id, string id) throws Error { + var full_id = ID_FORMAT.printf (app_id, id); + + if (!bubbles.has_key (full_id)) { + throw new DBusError.FAILED ("Provided id %s not found", id); + } + + bubbles[full_id].close (); + } + + private void send_sound (string sound_name) { + if (sound_name == "") { + return; + } + + Canberra.Proplist props; + Canberra.Proplist.create (out props); + + props.sets (Canberra.PROP_CANBERRA_CACHE_CONTROL, "volatile"); + props.sets (Canberra.PROP_EVENT_ID, sound_name); + + CanberraGtk4.context_get ().play_full (0, props); + } +} From 50d7d2e96e1ae83b9fbf4452d98338912a372f81 Mon Sep 17 00:00:00 2001 From: lenemter Date: Mon, 6 Oct 2025 11:37:42 +0300 Subject: [PATCH 2/4] Cleanup --- src/AbstractBubble.vala | 24 ++++++++++++++---------- src/Application.vala | 33 ++++++++++++++------------------- src/DBus.vala | 29 ++++------------------------- src/FdoActionGroup.vala | 4 ++-- src/PortalProxy.vala | 21 +++++---------------- 5 files changed, 39 insertions(+), 72 deletions(-) diff --git a/src/AbstractBubble.vala b/src/AbstractBubble.vala index 2092940e..efd3f1ac 100644 --- a/src/AbstractBubble.vala +++ b/src/AbstractBubble.vala @@ -1,5 +1,5 @@ /* -* Copyright 2020 elementary, Inc. (https://elementary.io) +* Copyright 2020-2025 elementary, Inc. (https://elementary.io) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public @@ -18,13 +18,17 @@ * */ -public class Notifications.AbstractBubble : Gtk.Window { - public enum CloseReason { - EXPIRED, - DISMISSED, - UNDEFINED - } +public enum Notifications.CloseReason { + EXPIRED = 1, + DISMISSED = 2, + /** + * This value is unique for org.freedesktop.Notifications server interface and must not be used elsewhere. + */ + CLOSE_NOTIFICATION_CALL = 3, + UNDEFINED = 4 +} +public class Notifications.AbstractBubble : Gtk.Window { public signal void closed (CloseReason reason) { close (); } @@ -100,10 +104,10 @@ public class Notifications.AbstractBubble : Gtk.Window { carousel.page_changed.connect ((index) => { if (index == 0) { - closed (DISMISSED); + closed (CloseReason.DISMISSED); } }); - close_button.clicked.connect (() => closed (DISMISSED)); + close_button.clicked.connect (() => closed (CloseReason.DISMISSED)); var motion_controller = new Gtk.EventControllerMotion (); motion_controller.enter.connect (pointer_enter); @@ -176,7 +180,7 @@ public class Notifications.AbstractBubble : Gtk.Window { } private bool timeout_expired () { - closed (EXPIRED); + closed (CloseReason.EXPIRED); return Source.REMOVE; } diff --git a/src/Application.vala b/src/Application.vala index 3a623ffa..b7bc13ed 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -21,13 +21,10 @@ public class Notifications.Application : Gtk.Application { public static Settings settings = new Settings ("io.elementary.notifications"); - private static Granite.Settings granite_settings; - private static Gtk.Settings gtk_settings; - public Application () { Object ( application_id: "io.elementary.notifications", - flags: ApplicationFlags.IS_SERVICE | ApplicationFlags.ALLOW_REPLACEMENT + flags: ApplicationFlags.IS_SERVICE ); } @@ -36,7 +33,7 @@ public class Notifications.Application : Gtk.Application { new Notifications.Server (connection); new Notifications.PortalProxy (connection); } catch (Error e) { - Error.prefix_literal (out e, "Registring notification server failed: "); + Error.prefix_literal (out e, "Registering notification server failed: "); throw e; } @@ -48,13 +45,6 @@ public class Notifications.Application : Gtk.Application { Granite.init (); - granite_settings = Granite.Settings.get_default (); - gtk_settings = Gtk.Settings.get_default (); - gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; - granite_settings.notify["prefers-color-scheme"].connect (() => { - gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; - }); - unowned var context = CanberraGtk4.context_get (); context.change_props ( Canberra.PROP_APPLICATION_NAME, "Notifications", @@ -64,15 +54,10 @@ public class Notifications.Application : Gtk.Application { context.open (); - var dbus_flags = BusNameOwnerFlags.DO_NOT_QUEUE | BusNameOwnerFlags.ALLOW_REPLACEMENT; - if (ApplicationFlags.REPLACE in flags) { - dbus_flags |= BusNameOwnerFlags.REPLACE; - } - Bus.own_name_on_connection ( get_dbus_connection (), "org.freedesktop.Notifications", - dbus_flags, + DO_NOT_QUEUE, () => hold (), (conn, name) => { critical ("Could not acquire bus: %s", name); @@ -83,7 +68,7 @@ public class Notifications.Application : Gtk.Application { Bus.own_name_on_connection ( get_dbus_connection (), "io.elementary.notifications.PortalProxy", - dbus_flags, + DO_NOT_QUEUE, () => hold (), (conn, name) => { critical ("Could not acquire bus: %s", name); @@ -92,6 +77,16 @@ public class Notifications.Application : Gtk.Application { ); } + public static void play_sound (string sound_name) { + Canberra.Proplist props; + Canberra.Proplist.create (out props); + + props.sets (Canberra.PROP_CANBERRA_CACHE_CONTROL, "volatile"); + props.sets (Canberra.PROP_EVENT_ID, sound_name); + + CanberraGtk4.context_get ().play_full (0, props); + } + public static int main (string[] args) { var app = new Application (); return app.run (args); diff --git a/src/DBus.vala b/src/DBus.vala index 2f88c88e..eca9ad8c 100644 --- a/src/DBus.vala +++ b/src/DBus.vala @@ -1,17 +1,10 @@ /* - * Copyright 2019-2023 elementary, Inc. (https://elementary.io) + * Copyright 2019-2025 elementary, Inc. (https://elementary.io) * SPDX-License-Identifier: GPL-3.0-or-later */ [DBus (name = "org.freedesktop.Notifications")] public class Notifications.Server : Object { - public enum FdoCloseReason { - EXPIRED = 1, - DISMISSED = 2, - CLOSE_NOTIFICATION_CALL = 3, - UNDEFINED = 4 - } - public signal void action_invoked (uint32 id, string action_key); public signal void notification_closed (uint32 id, uint32 reason); @@ -63,7 +56,7 @@ public class Notifications.Server : Object { throw new DBusError.FAILED (""); } - notification_closed (id, FdoCloseReason.CLOSE_NOTIFICATION_CALL); + notification_closed (id, CloseReason.CLOSE_NOTIFICATION_CALL); } public string [] get_capabilities () throws DBusError, IOError { @@ -175,7 +168,7 @@ public class Notifications.Server : Object { sound = category_to_sound_name (hints["category"].get_string ()); } - send_sound (sound); + Application.play_sound (sound); } } } @@ -196,7 +189,7 @@ public class Notifications.Server : Object { // consistency it should. So we make it emit the default one. var confirmation_type = hints.lookup (X_CANONICAL_PRIVATE_SYNCHRONOUS).get_string (); if (confirmation_type == "indicator-sound") { - send_sound ("audio-volume-change"); + Application.play_sound ("audio-volume-change"); } if (confirmation == null) { @@ -216,20 +209,6 @@ public class Notifications.Server : Object { confirmation.present (); } - private void send_sound (string sound_name) { - if (sound_name == "") { - return; - } - - Canberra.Proplist props; - Canberra.Proplist.create (out props); - - props.sets (Canberra.PROP_CANBERRA_CACHE_CONTROL, "volatile"); - props.sets (Canberra.PROP_EVENT_ID, sound_name); - - CanberraGtk4.context_get ().play_full (0, props); - } - static unowned string category_to_sound_name (string category) { unowned string sound; diff --git a/src/FdoActionGroup.vala b/src/FdoActionGroup.vala index 767fe8cd..6c1cc739 100644 --- a/src/FdoActionGroup.vala +++ b/src/FdoActionGroup.vala @@ -95,7 +95,7 @@ public sealed class Notifications.Fdo.ActionGroup : Object, GLib.ActionGroup { uint32 id; while (iter.next ("u", out id)) { - server.notification_closed (id, Server.FdoCloseReason.DISMISSED); + server.notification_closed (id, CloseReason.DISMISSED); } return; @@ -120,7 +120,7 @@ public sealed class Notifications.Fdo.ActionGroup : Object, GLib.ActionGroup { /* GLib says that we are only meant to override list_actions and query_actions, * however, the gio bindings only have query_action marked as virtual. * - * FIXME: remove everthing below when we have valac 0.58 as minimal version. + * FIXME: remove everything below when we have valac 0.58 as minimal version. */ public bool has_action (string action_name) { return action_name in (Gee.Collection) actions; diff --git a/src/PortalProxy.vala b/src/PortalProxy.vala index 5439dc80..67d4980a 100644 --- a/src/PortalProxy.vala +++ b/src/PortalProxy.vala @@ -1,4 +1,7 @@ -// Copyright +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ // TEST CALL FOR DSPY: @@ -86,7 +89,7 @@ public class Notifications.PortalProxy : Object { if (app_settings.get_boolean ("sounds")) { var sound = notification.priority != URGENT ? "dialog-information" : "dialog-warning"; - send_sound (sound); + Application.play_sound (sound); } } } @@ -100,18 +103,4 @@ public class Notifications.PortalProxy : Object { bubbles[full_id].close (); } - - private void send_sound (string sound_name) { - if (sound_name == "") { - return; - } - - Canberra.Proplist props; - Canberra.Proplist.create (out props); - - props.sets (Canberra.PROP_CANBERRA_CACHE_CONTROL, "volatile"); - props.sets (Canberra.PROP_EVENT_ID, sound_name); - - CanberraGtk4.context_get ().play_full (0, props); - } } From 2f8cea29912a56a01f89c2b560393a99401c7da9 Mon Sep 17 00:00:00 2001 From: lenemter Date: Mon, 6 Oct 2025 23:16:18 +0300 Subject: [PATCH 3/4] Reduce diff, fix merge --- src/Application.vala | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 80fffd9e..22a7ed4a 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -24,7 +24,7 @@ public class Notifications.Application : Gtk.Application { public Application () { Object ( application_id: "io.elementary.notifications", - flags: ApplicationFlags.IS_SERVICE + flags: ApplicationFlags.IS_SERVICE | ApplicationFlags.ALLOW_REPLACEMENT ); } @@ -54,10 +54,15 @@ public class Notifications.Application : Gtk.Application { context.open (); + var dbus_flags = BusNameOwnerFlags.DO_NOT_QUEUE | BusNameOwnerFlags.ALLOW_REPLACEMENT; + if (ApplicationFlags.REPLACE in flags) { + dbus_flags |= BusNameOwnerFlags.REPLACE; + } + Bus.own_name_on_connection ( get_dbus_connection (), "org.freedesktop.Notifications", - DO_NOT_QUEUE, + dbus_flags, () => hold (), (conn, name) => { critical ("Could not acquire bus: %s", name); @@ -68,7 +73,7 @@ public class Notifications.Application : Gtk.Application { Bus.own_name_on_connection ( get_dbus_connection (), "io.elementary.notifications.PortalProxy", - DO_NOT_QUEUE, + dbus_flags, () => hold (), (conn, name) => { critical ("Could not acquire bus: %s", name); @@ -87,16 +92,6 @@ public class Notifications.Application : Gtk.Application { CanberraGtk4.context_get ().play_full (0, props); } - public static void play_sound (string sound_name) { - Canberra.Proplist props; - Canberra.Proplist.create (out props); - - props.sets (Canberra.PROP_CANBERRA_CACHE_CONTROL, "volatile"); - props.sets (Canberra.PROP_EVENT_ID, sound_name); - - CanberraGtk4.context_get ().play_full (0, props); - } - public static int main (string[] args) { var app = new Application (); return app.run (args); From da04b038d7a8590fb5ceecbf0dcb0172c0d920ca Mon Sep 17 00:00:00 2001 From: lenemter Date: Mon, 6 Oct 2025 23:30:11 +0300 Subject: [PATCH 4/4] Reduce diff --- src/DBus.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DBus.vala b/src/DBus.vala index eca9ad8c..1ec31f4a 100644 --- a/src/DBus.vala +++ b/src/DBus.vala @@ -151,7 +151,7 @@ public class Notifications.Server : Object { return Gdk.EVENT_PROPAGATE; }); bubbles[id].closed.connect ((res) => { - if (res == EXPIRED && app_settings.get_boolean ("remember")) { + if (res == CloseReason.EXPIRED && app_settings.get_boolean ("remember")) { return; }