Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Implement Notifications v2 portal #128

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion data/pantheon.portal
Original file line number Diff line number Diff line change
@@ -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
136 changes: 136 additions & 0 deletions src/Notification/ActionGroup.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@

/**
* Handles all action logic for notifications. It automatically tracks currently available notifications
* and lists their available actions. On activation it will emit the action_invoked signal on the portal, or
* close the notification or launch the application.
*/
public class Notification.ActionGroup : Object, GLib.ActionGroup {
public Portal portal { get; construct; }

public ActionGroup (Portal portal) {
Object (portal: portal);
}

public string[] list_actions () {
var builder = new StrvBuilder ();

for (uint i = 0; i < portal.notifications.n_items; i++) {
var notification = (Notification) portal.notifications.get_item (i);

builder.add (notification.dismiss_action_name);
builder.add (notification.default_action_name);

foreach (var button in notification.buttons) {
builder.add (button.action_name);
}
}

return builder.end ();
}

public void activate_action (string name, Variant? target) {
var parts = name.split ("+", 3);

if (parts.length != 3) {
warning ("Invalid action name: %s", name);
return;
}

var internal_id = parts[0];
var type = parts[1];
var action_name = parts[2];

var id_parts = internal_id.split (":", 2);

if (id_parts.length != 2) {
warning ("Invalid internal id: %s", internal_id);
return;
}

var app_id = id_parts[0];
var notification_id = id_parts[1];

if (type == "action") {
portal.action_invoked (app_id, notification_id, action_name, { target });
} else {
switch (action_name) {
case "default":
// launch
break;

case "dismiss":
portal.replace_notification (internal_id, null);
break;

default:
break;
}
}
}

public override bool query_action (
string name,
out bool enabled,
out unowned VariantType parameter_type,
out unowned VariantType state_type,
out Variant state_hint,
out Variant state
) {
enabled = true;
parameter_type = null;
state_type = null;
state_hint = null;
state = null;

var parts = name.split ("+", 3);

if (parts.length != 3) {
warning ("Invalid action name: %s", name);
return false;
}

var internal_id = parts[0];
var type = parts[1];
var action_name = parts[2];

Notification? notification = null;
for (uint i = 0; i < portal.notifications.n_items; i++) {
var n = (Notification) portal.notifications.get_item (i);
if (n.internal_id == internal_id) {
notification = n;
break;
}
}

if (notification == null) {
warning ("Notification not found: %s", internal_id);
return false;
}

if (type == "action") {
foreach (var button in notification.buttons) {
if (button.action_name == action_name) {
parameter_type = button.action_target.length > 0 ? button.action_target[0].get_type () : null;
return true;
}
}
} else {
switch (action_name) {
case "default":
parameter_type = notification.default_action_target.length > 0 ? notification.default_action_target[0].get_type () : null;
return true;

case "dismiss":
parameter_type = null;
return true;

default:
return false;
}
}

return true;
}

public void change_action_state (string action_name, Variant value) { }
}
24 changes: 24 additions & 0 deletions src/Notification/BubbleManager.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@


public class Notification.BubbleManager : Object {
public Portal portal { get; construct; }

public BubbleManager (Portal portal) {
Object (portal: portal);
}

construct {
portal.notifications.items_changed.connect (on_items_changed);
}

private void on_items_changed (uint pos, uint removed, uint added) {
if (pos != 0 || removed != 0) {
return;
}

var added_notification = (Notification) portal.notifications.get_item (pos);

var bubble = new Bubble (portal, added_notification);
bubble.present ();
}
}
23 changes: 23 additions & 0 deletions src/Notification/DBus.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[DBus (name = "io.elementary.portal.NotificationProvider")]
public class Notification.DBusProvider : Object {
public signal void items_changed (uint pos, uint removed, uint added);

[DBus (visible = false)]
public Portal portal { get; construct; }

public DBusProvider (Portal portal) {
Object (portal: portal);
}

construct {
portal.notifications.items_changed.connect ((pos, removed, added) => items_changed (pos, removed, added));
}

public uint get_n_items () throws DBusError, IOError {
return portal.notifications.n_items;
}

public Notification.Data get_notification (uint index) throws DBusError, IOError {
return ((Notification) portal.notifications.get_item (index)).data;
}
}
99 changes: 99 additions & 0 deletions src/Notification/Notification.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@

public class Notification.Notification : GLib.Object {
public const string ACTION_GROUP_NAME = "action";
public const string ACTION_PREFIX = ACTION_GROUP_NAME + ".";
public const string ACTION_FORMAT = "%s+action+%s"; // interal id, action id
public const string INTERNAL_ACTION_FORMAT = "%s+internal+%s"; // interal id, action id

[Flags]
public enum DisplayHint {
TRANSIENT,
TRAY,
PERSISTENT,
HIDE_ON_LOCK_SCREEN,
HIDE_CONTENT_ON_LOCK_SCREEN,
SHOW_AS_NEW
}

public struct Button {
public string label;
public string action_name;
public Variant[] action_target;

public Button (string internal_id, HashTable<string, Variant> data) {
if ("label" in data) {
label = data["label"].get_string ();
}

if ("action" in data) {
action_name = ACTION_FORMAT.printf (internal_id, data["action"].get_string ());
}

if ("action-target" in data) {
action_target = { data["action-target"] };
} else {
action_target = {};
}
}
}

public struct Data {
public string internal_id;
public HashTable<string, Variant> raw_data;
public string app_id;
public string dismiss_action_name;
public string default_action_name;
public Variant[] default_action_target;
public Button[] buttons;
public DisplayHint display_hint;

public Data (string _internal_id, string _app_id, HashTable<string, Variant> _raw_data) {
internal_id = _internal_id;
raw_data = _raw_data;
app_id = _app_id;
dismiss_action_name = INTERNAL_ACTION_FORMAT.printf (internal_id, "dismiss");

if ("default-action" in raw_data) {
default_action_name = ACTION_FORMAT.printf (internal_id, raw_data["default-action"].get_string ());

if ("default-action-target" in raw_data) {
default_action_target = { raw_data["default-action-target"] };
} else {
default_action_target = {};
}
} else {
default_action_name = INTERNAL_ACTION_FORMAT.printf (internal_id, "default");
default_action_target = {};
}

if ("buttons" in raw_data) {
var raw_buttons = (HashTable<string, Variant>[]) raw_data["buttons"];

buttons = new Button[raw_buttons.length];

for (int i = 0; i < raw_buttons.length; i++) {
buttons[i] = Button (internal_id, raw_buttons[i]);
}
} else {
buttons = new Button[0];
}

display_hint = 0;
}
}

public Data data { get; construct; }

public string internal_id { get { return data.internal_id; } }

public string dismiss_action_name { get { return data.dismiss_action_name; } }
public string default_action_name { get { return data.default_action_name; } }
public Variant[] default_action_target { get { return data.default_action_target; } }
public Button[] buttons { get { return data.buttons; } }

public DisplayHint display_hint { get { return data.display_hint; } }

public Notification (string internal_id, string app_id, HashTable<string, Variant> raw_data) {
Object (data: Data (internal_id, app_id, raw_data));
}
}
90 changes: 90 additions & 0 deletions src/Notification/Portal.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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.'>})

/**
* The notififcations portal consists of a few parts. Most importantly this class which exposes the portal
* api, tracks currently active notifications and holds the other parts.
* The {@link ActionGroup} handles all action logic for notifications. It automatically exposes all actions
* for all available notifications and handles the activation of these actions (by talking to #this).
* The {@link BubbleManager} is responsible for showing the notifications to the user in a bubble.
* The {@link DBusProvider} is responsible for exposing the notifications to the DBus for consumption by the indicator. It also exports the {@link actions}.
* Both {@link BubbleManager} and {@link DBusProvider} use the {@link actions} for all interaction (dismissing, activating actions).
*/
[DBus (name = "org.freedesktop.impl.portal.Notification")]
public class Notification.Portal : Object {
public const string ID_FORMAT = "%s:%s";

public signal void action_invoked (string app_id, string id, string action_name, Variant[] parameters);

public HashTable<string, Variant> supported_options { get; construct; }

[DBus (visible = false)]
public DBusConnection connection { get; construct; }

[DBus (visible = false)]
public ListStore notifications { get; construct; }
[DBus (visible = false)]
public ActionGroup actions { get; construct; }

private DBusProvider dbus_provider;

public Portal (DBusConnection connection) {
Object (connection: connection);
}

construct {
supported_options = new HashTable<string, Variant> (str_hash, str_equal);

notifications = new ListStore (typeof (Notification));
actions = new ActionGroup (this);

dbus_provider = new DBusProvider (this);

try {
connection.register_object ("/io/elementary/portal/NotificationProvider", dbus_provider);
connection.export_action_group ("/io/elementary/portal/NotificationProvider", actions);
} catch (Error e) {
warning ("Failed to register provider: %s", e.message);
}
}

public void add_notification (string app_id, string id, HashTable<string, Variant> data) throws DBusError, IOError {
var internal_id = ID_FORMAT.printf (app_id, id);
var notification = new Notification (app_id, id, data);

replace_notification (internal_id, notification);
}

public void remove_notification (string app_id, string id) throws DBusError, IOError {
var internal_id = ID_FORMAT.printf (app_id, id);

replace_notification (internal_id, null);
}

/**
* Removes the given id and if not null replaces it with the given notification at the same position.
* If SHOW_AS_NEW is set in the display hint of the replacement, it will be added at the front instead of at the same position.
* If no notification with the given id is found, and the replacement is not null, the replacement will be added at the front.
*/
internal void replace_notification (string internal_id, Notification? replacement) {
for (int i = 0; i < notifications.n_items; i++) {
var notification = (Notification) notifications.get_object (i);
if (notification.internal_id == internal_id) {
if (replacement == null) { // Just remove and return
notifications.remove (i);
return;
} else if (SHOW_AS_NEW in replacement.display_hint) { // Remove but don't return because we want to add the replacement as if it was a new notification
notifications.remove (i);
} else { // Replace and return
notifications.splice (i, 1, { replacement });
return;
}
}
}

if (replacement != null) {
notifications.splice (0, 0, { replacement });
}
}
}
14 changes: 14 additions & 0 deletions src/Notification/Widgets/Bubble.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
public class Notification.Bubble : Gtk.Window {
public Portal portal { get; construct; }
public Notification notification { get; construct; }

public Bubble (Portal portal, Notification notification) {
Object (portal: portal, notification: notification);
}

construct {
child = new Widget (notification);

insert_action_group (Notification.ACTION_GROUP_NAME, portal.actions);
}
}
Loading
Loading