Skip to content
Draft
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
148 changes: 148 additions & 0 deletions src/Notification/ActionGroup.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@

/**
* 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 {
private const string TARGET_TYPE_STRING = "(sv)";
public static VariantType target_type = new VariantType (TARGET_TYPE_STRING);

public Portal portal { get; construct; }

public ActionGroup (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) {
for (uint i = pos; i < pos + added; i++) {
var notification = (Notification) portal.notifications.get_item (i);

foreach (var action in notification.list_actions ()) {
action_added (action);
}
}

//TODO: Maybe handle remove
}

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.addv (notification.list_actions ());
}

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 notification = Notification.get_for_internal_id (internal_id);

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

var app_id = notification.app_id;
var id = notification.id;

switch (type) {
case Notification.ACTION_TYPE_ACTION:
if (target == null || !target.is_of_type (target_type)) {
warning ("Invalid action target for action %s", name);
return;
}

string activation_token;
Variant action_target;
target.get ("(sv)", out activation_token, out action_target);

Variant[] action_target_array;
action_target.get ("av", out action_target_array);

var platform_data = new HashTable<string, Variant> (str_hash, str_equal);
platform_data["activation-token"] = activation_token;

var parameters = new Gee.LinkedList<Variant> ();

parameters.add_all_array (action_target_array);
parameters.add (platform_data);

portal.action_invoked (app_id, id, action_name, parameters.to_array ());
break;

case Notification.ACTION_TYPE_INTERNAL:
switch (action_name) {
case Notification.ACTION_DEFAULT:
// launch
warning ("Launched");
break;

case Notification.ACTION_DISMISS:
break;

default:
return;
}
break;

default:
return;
}

uint position;
if (portal.notifications.find (notification, out position)) {
portal.notifications.remove (position);
}
}

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 notification = Notification.get_for_internal_id (parts[0]);

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

return notification.query_action (name, out enabled, out parameter_type, out state_type, out state_hint, out state);
}

public void change_action_state (string action_name, Variant value) { }
}
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;
}
}
167 changes: 167 additions & 0 deletions src/Notification/Notification.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@

public class Notification.Notification : GLib.Object {
public const string ACTION_TYPE_ACTION = "action";
public const string ACTION_TYPE_INTERNAL = "internal";
public const string ACTION_DISMISS = "dismiss";
public const string ACTION_DEFAULT = "default";

private const string ACTION_FORMAT = "%s+action+%s"; // interal id, action name
private const string INTERNAL_ACTION_FORMAT = "%s+internal+%s"; // interal id, action name

[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 = new Variant[] { data["action-target"] };
} else {
action_target = new Variant[] {};
}
}
}

public struct Data {
public int64 timestamp;
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) {
timestamp = new DateTime.now_utc ().to_unix ();
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 = new Variant[] { raw_data["default-action-target"] };
} else {
default_action_target = new Variant[] {};
}
} else {
default_action_name = INTERNAL_ACTION_FORMAT.printf (internal_id, "default");
default_action_target = new Variant[] {};
}

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;
}
}

private static HashTable<string, unowned Notification> notifications_by_internal_id = new HashTable<string, unowned Notification> (str_hash, str_equal);
private static uint internal_ids = 0;

public static Notification? get_for_internal_id (string internal_id) {
return notifications_by_internal_id[internal_id];
}

private Data _data;
public Data data { get { return _data; } construct { _data = value; } }

public string internal_id { private get; construct; }
public string app_id { get; construct; }
public string id { get; construct; }

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

public Notification (string app_id, string id, HashTable<string, Variant> raw_data) {
var internal_id = "%u".printf (internal_ids++);
Object (internal_id: internal_id, app_id: app_id, id: id, data: Data (internal_id, app_id, raw_data));
}

construct {
notifications_by_internal_id[internal_id] = this;
}

~Notification () {
notifications_by_internal_id.remove (internal_id);
}

public string[] list_actions () {
string[] actions = new string[data.buttons.length + 2];

actions[0] = data.dismiss_action_name;
actions[1] = data.default_action_name;

for (int i = 0; i < data.buttons.length; i++) {
actions[i + 2] = data.buttons[i].action_name;
}

return actions;
}

public 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;

if (name == _data.dismiss_action_name) {
parameter_type = null;
return true;
}

if (name == _data.default_action_name) {
parameter_type = ActionGroup.target_type;
return true;
}

foreach (var button in _data.buttons) {
if (button.action_name == name) {
parameter_type = ActionGroup.target_type;
return true;
}
}

return false;
}

public void replace_timestamp (Notification old_notification) {
_data.timestamp = old_notification._data.timestamp;
}
}
Loading