diff --git a/data/pantheon.portal b/data/pantheon.portal index b9de94e3..b677a33e 100644 --- a/data/pantheon.portal +++ b/data/pantheon.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.pantheon -Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.ScreenCast +Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Notification;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.ScreenCast UseIn=pantheon diff --git a/src/Notification/FdoInterface.vala b/src/Notification/FdoInterface.vala new file mode 100644 index 00000000..fb518645 --- /dev/null +++ b/src/Notification/FdoInterface.vala @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +[DBus (name = "org.freedesktop.Notifications")] +public interface Notification.FdoInterface : Object { + public enum CloseReason { + EXPIRED = 1, + DISMISSED = 2, + CLOSE_NOTIFICATION_CALL = 3, + UNDEFINED = 4 + } + + public signal void notification_closed (uint32 id, uint32 reason); + public signal void action_invoked (uint32 id, string action_key); + + public abstract uint32 notify (string app_name, uint32 replaces_id, string app_icon, string summary, string body, string[] actions, HashTable hints, int32 expire_timeout) throws Error; + public abstract void close_notification (uint32 id) throws Error; +} diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala new file mode 100644 index 00000000..141d640b --- /dev/null +++ b/src/Notification/Portal.vala @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * Authored by: Leonhard Kargl + */ + +// 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 = "org.freedesktop.impl.portal.Notification")] +public class Notification.Portal : 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 FdoInterface fdo_interface; + private HashTable portal_id_to_fdo_id; + + public Portal (DBusConnection connection) { + Object (connection: connection); + } + + construct { + supported_options = new HashTable (str_hash, str_equal); + portal_id_to_fdo_id = new HashTable (str_hash, str_equal); + + try { + fdo_interface = Bus.get_proxy_sync (SESSION, "org.freedesktop.Notifications", "/org/freedesktop/Notifications"); + } catch (Error e) { + critical (e.message); + } + } + + 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 (); + } + + uint8 priority = 1; + if ("priority" in data) { + switch (data["priority"].get_string ()) { + case "low": + priority = 0; + break; + + case "normal": + priority = 1; + break; + case "high": + priority = 2; + break; + case "urgent": + priority = 2; + break; + } + } + + var hints = new HashTable (str_hash, str_equal); + hints["urgency"] = priority; + + try { + var fdo_id = fdo_interface.notify (app_id, 0, app_id, title, body, {}, hints, 0); + portal_id_to_fdo_id[ID_FORMAT.printf (app_id, id)] = fdo_id; + } catch (Error e) { + critical (e.message); + throw new DBusError.FAILED ("Failed to send notification: %s", e.message); + } + } + + public void remove_notification (string app_id, string id) throws Error { + var formatted_id = ID_FORMAT.printf (app_id, id); + + if (!(formatted_id in portal_id_to_fdo_id)) { + throw new DBusError.FAILED ("Provided id %s not found", id); + } + + try { + fdo_interface.close_notification (portal_id_to_fdo_id[formatted_id]); + } catch (Error e) { + critical (e.message); + throw new DBusError.FAILED ("Failed to close notification: %s", e.message); + } + } +} diff --git a/src/XdgDesktopPortalPantheon.vala b/src/XdgDesktopPortalPantheon.vala index 69d8a778..da6639b2 100644 --- a/src/XdgDesktopPortalPantheon.vala +++ b/src/XdgDesktopPortalPantheon.vala @@ -41,6 +41,9 @@ private void on_bus_acquired (DBusConnection connection, string name) { connection.register_object ("/org/freedesktop/portal/desktop", new Background.Portal (connection)); debug ("Background Portal registered!"); + connection.register_object ("/org/freedesktop/portal/desktop", new Notification.Portal (connection)); + debug ("Notification Portal registered!"); + connection.register_object ("/org/freedesktop/portal/desktop", new Screenshot.Portal (connection)); debug ("Screenshot Portal registered!"); diff --git a/src/meson.build b/src/meson.build index 68f41c3d..2ee90b6e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,6 +8,8 @@ executable( 'AppChooser/Portal.vala', 'Background/NotificationRequest.vala', 'Background/Portal.vala', + 'Notification/FdoInterface.vala', + 'Notification/Portal.vala', 'ScreenCast/MonitorTracker/Interface.vala', 'ScreenCast/MonitorTracker/Monitor.vala', 'ScreenCast/MonitorTracker/MonitorTracker.vala',