Implement updating of notifications
This commit is contained in:
parent
59c175f312
commit
2bffea2bdc
9 changed files with 96 additions and 54 deletions
|
|
@ -127,7 +127,9 @@ escape code to inform when the notification is closed::
|
|||
|
||||
If no notification id was specified ``i=0`` will be used.
|
||||
If ``a=report`` is specified and the notification is activated/clicked on
|
||||
then both the activation report and close notification are sent.
|
||||
then both the activation report and close notification are sent. If the notification
|
||||
is updated then the close event is not sent unless the updated notification
|
||||
also requests a close notification.
|
||||
|
||||
.. note:: On macOS the OS does not supply notification
|
||||
closed events to applications. As such close events must be implemented
|
||||
|
|
@ -137,11 +139,19 @@ then both the activation report and close notification are sent.
|
|||
being authoritative.
|
||||
|
||||
|
||||
Closing an existing notification
|
||||
----------------------------------
|
||||
Updating or closing an existing notification
|
||||
----------------------------------------------
|
||||
|
||||
.. versionadded:: 0.36.0
|
||||
The ability to close a previous notification was added in kitty 0.36.0
|
||||
The ability to update and close a previous notification was added in kitty 0.36.0
|
||||
|
||||
To update a previous notification simply send a new notification with the same
|
||||
*notification id* (``i`` key) as the one you want to update. If the original
|
||||
notification is still displayed it will be replaced, otherwise a new
|
||||
notification is displayed. This can be used, for example, to show progress of
|
||||
an operation. Note that how smoothly the existing notification is replaced
|
||||
depends on the underlying OS, for example, on Linux the replacement is usually flicker
|
||||
free, on macOS it isn't, because of Apple's design choices.
|
||||
|
||||
To close a previous notification, send::
|
||||
|
||||
|
|
|
|||
2
glfw/glfw3.h
vendored
2
glfw/glfw3.h
vendored
|
|
@ -1317,7 +1317,7 @@ typedef struct GLFWLayerShellConfig {
|
|||
|
||||
typedef struct GLFWDBUSNotificationData {
|
||||
const char *app_name, *icon, *summary, *body, *action_name;
|
||||
int32_t timeout; uint8_t urgency;
|
||||
int32_t timeout; uint8_t urgency; uint32_t replaces;
|
||||
} GLFWDBUSNotificationData;
|
||||
|
||||
/*! @brief The function pointer type for error callbacks.
|
||||
|
|
|
|||
3
glfw/linux_notify.c
vendored
3
glfw/linux_notify.c
vendored
|
|
@ -111,7 +111,6 @@ glfw_dbus_send_user_notification(const GLFWDBUSNotificationData *n, GLFWDBusnoti
|
|||
data->next_id = ++notification_id;
|
||||
data->callback = callback; data->data = user_data;
|
||||
if (!data->next_id) data->next_id = ++notification_id;
|
||||
uint32_t replaces_id = 0;
|
||||
|
||||
RAII_MSG(msg, dbus_message_new_method_call(NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE, "Notify"));
|
||||
if (!msg) { return 0; }
|
||||
|
|
@ -120,7 +119,7 @@ glfw_dbus_send_user_notification(const GLFWDBUSNotificationData *n, GLFWDBusnoti
|
|||
#define check_call(func, ...) if (!func(__VA_ARGS__)) { _glfwInputError(GLFW_PLATFORM_ERROR, "%s", "Out of memory allocating DBUS message for notification\n"); return 0; }
|
||||
#define APPEND(to, type, val) check_call(dbus_message_iter_append_basic, &to, type, &val);
|
||||
APPEND(args, DBUS_TYPE_STRING, n->app_name)
|
||||
APPEND(args, DBUS_TYPE_UINT32, replaces_id)
|
||||
APPEND(args, DBUS_TYPE_UINT32, n->replaces)
|
||||
APPEND(args, DBUS_TYPE_STRING, n->icon)
|
||||
APPEND(args, DBUS_TYPE_STRING, n->summary)
|
||||
APPEND(args, DBUS_TYPE_STRING, n->body)
|
||||
|
|
|
|||
|
|
@ -549,14 +549,13 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|||
}
|
||||
|
||||
static void
|
||||
schedule_notification(const char *identifier, const char *title, const char *body, const char *subtitle, int urgency) {
|
||||
schedule_notification(const char *identifier, const char *title, const char *body, int urgency) {
|
||||
UNUserNotificationCenter *center = get_notification_center_safely();
|
||||
if (!center) return;
|
||||
// Configure the notification's payload.
|
||||
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
|
||||
if (title) content.title = @(title);
|
||||
if (body) content.body = @(body);
|
||||
if (subtitle) content.subtitle = @(subtitle);
|
||||
content.sound = [UNNotificationSound defaultSound];
|
||||
#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 120000
|
||||
switch (urgency) {
|
||||
|
|
@ -594,7 +593,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|||
|
||||
|
||||
typedef struct {
|
||||
char *identifier, *title, *body, *subtitle;
|
||||
char *identifier, *title, *body;
|
||||
int urgency;
|
||||
} QueuedNotification;
|
||||
|
||||
|
|
@ -605,13 +604,12 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|||
static NotificationQueue notification_queue = {0};
|
||||
|
||||
static void
|
||||
queue_notification(const char *identifier, const char *title, const char* body, const char* subtitle, int urgency) {
|
||||
queue_notification(const char *identifier, const char *title, const char* body, int urgency) {
|
||||
ensure_space_for((¬ification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true);
|
||||
QueuedNotification *n = notification_queue.notifications + notification_queue.count++;
|
||||
n->identifier = identifier ? strdup(identifier) : NULL;
|
||||
n->title = title ? strdup(title) : NULL;
|
||||
n->body = body ? strdup(body) : NULL;
|
||||
n->subtitle = subtitle ? strdup(subtitle) : NULL;
|
||||
n->urgency = urgency;
|
||||
}
|
||||
|
||||
|
|
@ -620,13 +618,13 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|||
if (granted) {
|
||||
for (size_t i = 0; i < notification_queue.count; i++) {
|
||||
QueuedNotification *n = notification_queue.notifications + i;
|
||||
schedule_notification(n->identifier, n->title, n->body, n->subtitle, n->urgency);
|
||||
schedule_notification(n->identifier, n->title, n->body, n->urgency);
|
||||
}
|
||||
}
|
||||
while(notification_queue.count) {
|
||||
QueuedNotification *n = notification_queue.notifications + --notification_queue.count;
|
||||
if (!granted) do_notification_callback(@(n->identifier), "closed");
|
||||
free(n->identifier); free(n->title); free(n->body); free(n->subtitle);
|
||||
free(n->identifier); free(n->title); free(n->body);
|
||||
memset(n, 0, sizeof(QueuedNotification));
|
||||
}
|
||||
}
|
||||
|
|
@ -640,13 +638,13 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|||
|
||||
static PyObject*
|
||||
cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
||||
char *identifier = NULL, *title = NULL, *body = NULL, *subtitle = NULL; int urgency = 1;
|
||||
if (!PyArg_ParseTuple(args, "ssz|zi", &identifier, &title, &body, &subtitle, &urgency)) return NULL;
|
||||
char *identifier = NULL, *title = NULL, *body = NULL; int urgency = 1;
|
||||
if (!PyArg_ParseTuple(args, "sss|i", &identifier, &title, &body, &urgency)) return NULL;
|
||||
|
||||
UNUserNotificationCenter *center = get_notification_center_safely();
|
||||
if (!center) Py_RETURN_NONE;
|
||||
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
|
||||
queue_notification(identifier, title, body, subtitle, urgency);
|
||||
queue_notification(identifier, title, body, urgency);
|
||||
|
||||
// The badge permission needs to be requested as well, even though it is not used,
|
||||
// otherwise macOS refuses to show the preference checkbox for enable/disable notification sound.
|
||||
|
|
|
|||
|
|
@ -556,6 +556,7 @@ def dbus_send_notification(
|
|||
action_text: str = '',
|
||||
timeout: int = -1,
|
||||
urgency: int = 1,
|
||||
replaces: int = 0,
|
||||
) -> int:
|
||||
pass
|
||||
|
||||
|
|
@ -566,8 +567,7 @@ def dbus_close_notification(dbus_notification_id: int) -> bool: ...
|
|||
def cocoa_send_notification(
|
||||
identifier: str,
|
||||
title: str,
|
||||
body: Optional[str],
|
||||
subtitle: Optional[str],
|
||||
body: str,
|
||||
urgency: int = 1,
|
||||
) -> None:
|
||||
pass
|
||||
|
|
|
|||
3
kitty/glfw-wrapper.h
generated
3
kitty/glfw-wrapper.h
generated
|
|
@ -1055,7 +1055,7 @@ typedef struct GLFWLayerShellConfig {
|
|||
|
||||
typedef struct GLFWDBUSNotificationData {
|
||||
const char *app_name, *icon, *summary, *body, *action_name;
|
||||
int32_t timeout; uint8_t urgency;
|
||||
int32_t timeout; uint8_t urgency; uint32_t replaces;
|
||||
} GLFWDBUSNotificationData;
|
||||
|
||||
/*! @brief The function pointer type for error callbacks.
|
||||
|
|
@ -1705,7 +1705,6 @@ typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
|||
typedef void (*GLFWwaylandframecallbackfunc)(unsigned long long id);
|
||||
typedef void (*GLFWDBusnotificationcreatedfun)(unsigned long long, uint32_t, void*);
|
||||
typedef void (*GLFWDBusnotificationactivatedfun)(uint32_t, int, const char*);
|
||||
|
||||
typedef int (*glfwInit_func)(monotonic_t);
|
||||
GFW_EXTERN glfwInit_func glfwInit_impl;
|
||||
#define glfwInit glfwInit_impl
|
||||
|
|
|
|||
|
|
@ -2076,17 +2076,18 @@ dbus_notification_created_callback(unsigned long long notification_id, uint32_t
|
|||
|
||||
static PyObject*
|
||||
dbus_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
|
||||
int timeout = -1, urgency = 1;
|
||||
int timeout = -1, urgency = 1; unsigned int replaces = 0;
|
||||
GLFWDBUSNotificationData d = {.action_name=""};
|
||||
static const char* kwlist[] = {"app_name", "app_icon", "title", "body", "action_text", "timeout", "urgency", NULL};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kw, "ssss|sii", (char**)kwlist,
|
||||
&d.app_name, &d.icon, &d.summary, &d.body, &d.action_name, &timeout, &urgency)) return NULL;
|
||||
static const char* kwlist[] = {"app_name", "app_icon", "title", "body", "action_text", "timeout", "urgency", "replaces", NULL};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kw, "ssss|siiI", (char**)kwlist,
|
||||
&d.app_name, &d.icon, &d.summary, &d.body, &d.action_name, &timeout, &urgency, &replaces)) return NULL;
|
||||
if (!glfwDBusUserNotify) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to load glfwDBusUserNotify, did you call glfw_init?");
|
||||
return NULL;
|
||||
}
|
||||
d.timeout = timeout;
|
||||
d.urgency = urgency & 3;
|
||||
d.replaces = replaces;
|
||||
unsigned long long notification_id = glfwDBusUserNotify(&d, dbus_notification_created_callback, NULL);
|
||||
return PyLong_FromUnsignedLongLong(notification_id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ class NotificationCommand:
|
|||
# event callbacks
|
||||
on_activation: Optional[Callable[['NotificationCommand'], None]] = None
|
||||
on_close: Optional[Callable[['NotificationCommand'], None]] = None
|
||||
on_update: Optional[Callable[['NotificationCommand', 'NotificationCommand'], None]] = None
|
||||
|
||||
# metadata
|
||||
identifier: str = ''
|
||||
|
|
@ -234,9 +235,12 @@ def focus_requested(self) -> bool:
|
|||
return Action.focus in self.actions
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f'NotificationCommand(identifier={self.identifier!r}, title={self.title!r}, body={self.body!r},'
|
||||
f'actions={self.actions}, done={self.done!r}, urgency={self.urgency})')
|
||||
fields = {}
|
||||
for x in ('title', 'body', 'identifier', 'actions', 'urgency', 'done'):
|
||||
val = getattr(self, x)
|
||||
if val:
|
||||
fields[x] = val
|
||||
return f'NotificationCommand{fields}'
|
||||
|
||||
def parse_metadata(self, metadata: str, prev: 'NotificationCommand') -> Tuple[PayloadType, bool]:
|
||||
payload_type = PayloadType.title
|
||||
|
|
@ -408,7 +412,7 @@ def initialize(self) -> None:
|
|||
def close_notification(self, desktop_notification_id: int) -> bool:
|
||||
raise NotImplementedError('Implement me in subclass')
|
||||
|
||||
def notify(self, nc: NotificationCommand) -> int:
|
||||
def notify(self, nc: NotificationCommand, existing_desktop_notification_id: Optional[int]) -> int:
|
||||
raise NotImplementedError('Implement me in subclass')
|
||||
|
||||
def on_new_version_notification_activation(self, cmd: NotificationCommand) -> None:
|
||||
|
|
@ -439,18 +443,18 @@ def close_notification(self, desktop_notification_id: int) -> bool:
|
|||
log_error(f'Close request for {desktop_notification_id=} {"succeeded" if close_succeeded else "failed"}')
|
||||
return close_succeeded
|
||||
|
||||
def notify(self, nc: NotificationCommand) -> int:
|
||||
desktop_notification_id = next(self.id_counter)
|
||||
def notify(self, nc: NotificationCommand, existing_desktop_notification_id: Optional[int]) -> int:
|
||||
desktop_notification_id = existing_desktop_notification_id or next(self.id_counter)
|
||||
from .fast_data_types import cocoa_send_notification
|
||||
# If the body is not set macos makes the title the body and uses
|
||||
# "kitty" as the title. So use a single space for the body in this
|
||||
# case. Although https://developer.apple.com/documentation/usernotifications/unnotificationcontent/body?language=objc
|
||||
# says printf style strings are stripped this doesnt actually happen,
|
||||
# says printf style strings are stripped this does not actually happen,
|
||||
# so dont double %
|
||||
# for %% escaping.
|
||||
body = (nc.body or ' ')
|
||||
assert nc.urgency is not None
|
||||
cocoa_send_notification(str(desktop_notification_id), nc.title, body, '', nc.urgency.value)
|
||||
cocoa_send_notification(str(desktop_notification_id), nc.title, body, nc.urgency.value)
|
||||
return desktop_notification_id
|
||||
|
||||
def notification_activated(self, event: str, ident: str) -> None:
|
||||
|
|
@ -476,7 +480,8 @@ def initialize(self) -> None:
|
|||
dbus_set_notification_callback(self.dispatch_event_from_desktop)
|
||||
# map the id returned by the notification daemon to the
|
||||
# desktop_notification_id we use for the notification
|
||||
self.creation_id_map: 'OrderedDict[int, int]' = OrderedDict()
|
||||
self.dbus_to_desktop: 'OrderedDict[int, int]' = OrderedDict()
|
||||
self.desktop_to_dbus: Dict[int, int] = {}
|
||||
|
||||
def close_notification(self, desktop_notification_id: int) -> bool:
|
||||
from .fast_data_types import dbus_close_notification
|
||||
|
|
@ -488,28 +493,32 @@ def close_notification(self, desktop_notification_id: int) -> bool:
|
|||
return close_succeeded
|
||||
|
||||
def get_desktop_notification_id(self, dbus_notification_id: int, event: str) -> Optional[int]:
|
||||
q = self.creation_id_map.get(dbus_notification_id)
|
||||
q = self.dbus_to_desktop.get(dbus_notification_id)
|
||||
if q is None:
|
||||
if debug_desktop_integration:
|
||||
log_error(f'Could not find desktop_notification_id for {dbus_notification_id=} for event {event}')
|
||||
return q
|
||||
|
||||
def get_dbus_notification_id(self, desktop_notification_id: int, event: str) ->Optional[int]:
|
||||
for dbus_id, q in self.creation_id_map.items():
|
||||
if q == desktop_notification_id:
|
||||
return dbus_id
|
||||
if debug_desktop_integration:
|
||||
log_error(f'Could not find dbus_notification_id for {desktop_notification_id=} for event {event}')
|
||||
return None
|
||||
q = self.desktop_to_dbus.get(desktop_notification_id)
|
||||
if q is None:
|
||||
if debug_desktop_integration:
|
||||
log_error(f'Could not find dbus_notification_id for {desktop_notification_id=} for event {event}')
|
||||
return q
|
||||
|
||||
def created(self, dbus_notification_id: int, desktop_notification_id: int) -> None:
|
||||
self.dbus_to_desktop[desktop_notification_id] = dbus_notification_id
|
||||
self.desktop_to_dbus[dbus_notification_id] = desktop_notification_id
|
||||
if len(self.dbus_to_desktop) > 128:
|
||||
k, v = self.dbus_to_desktop.popitem(False)
|
||||
self.desktop_to_dbus.pop(v, None)
|
||||
self.notification_manager.notification_created(dbus_notification_id)
|
||||
|
||||
def dispatch_event_from_desktop(self, event_type: str, dbus_notification_id: int, extra: Union[int, str]) -> None:
|
||||
if debug_desktop_integration:
|
||||
log_error(f'Got notification event from desktop: {event_type=} {dbus_notification_id=} {extra=}')
|
||||
if event_type == 'created':
|
||||
self.creation_id_map[int(extra)] = dbus_notification_id
|
||||
if len(self.creation_id_map) > 128:
|
||||
self.creation_id_map.popitem(False)
|
||||
self.notification_manager.notification_created(dbus_notification_id)
|
||||
self.created(dbus_notification_id, int(extra))
|
||||
return
|
||||
if desktop_notification_id := self.get_desktop_notification_id(dbus_notification_id, event_type):
|
||||
if event_type == 'activation_token':
|
||||
|
|
@ -519,15 +528,22 @@ def dispatch_event_from_desktop(self, event_type: str, dbus_notification_id: int
|
|||
elif event_type == 'closed':
|
||||
self.notification_manager.notification_closed(desktop_notification_id)
|
||||
|
||||
def notify(self, nc: NotificationCommand) -> int:
|
||||
def notify(self, nc: NotificationCommand, existing_desktop_notification_id: Optional[int]) -> int:
|
||||
from .fast_data_types import dbus_send_notification
|
||||
app_icon = nc.icon_name or nc.icon_path or get_custom_window_icon()[1] or logo_png_file
|
||||
body = nc.body.replace('<', '<\u200c').replace('&', '&\u200c') # prevent HTML markup from being recognized
|
||||
assert nc.urgency is not None
|
||||
replaces_dbus_id = 0
|
||||
if existing_desktop_notification_id:
|
||||
replaces_dbus_id = self.get_dbus_notification_id(existing_desktop_notification_id, 'notify') or 0
|
||||
desktop_notification_id = dbus_send_notification(
|
||||
app_name=nc.application_name or 'kitty', app_icon=app_icon, title=nc.title, body=body, timeout=-1, urgency=nc.urgency.value)
|
||||
app_name=nc.application_name or 'kitty', app_icon=app_icon, title=nc.title, body=body, timeout=-1, urgency=nc.urgency.value,
|
||||
replaces=replaces_dbus_id)
|
||||
if debug_desktop_integration:
|
||||
log_error(f'Created notification with {desktop_notification_id=}')
|
||||
log_error(f'Requested creation of notification with {desktop_notification_id=}')
|
||||
if existing_desktop_notification_id and replaces_dbus_id:
|
||||
self.dbus_to_desktop.pop(replaces_dbus_id, None)
|
||||
self.desktop_to_dbus.pop(existing_desktop_notification_id, None)
|
||||
return desktop_notification_id
|
||||
|
||||
|
||||
|
|
@ -644,6 +660,15 @@ def notification_activated(self, desktop_notification_id: int) -> None:
|
|||
except Exception as e:
|
||||
self.log('Notification on_activation handler failed with error:', e)
|
||||
|
||||
def notification_replaced(self, old_cmd: NotificationCommand, new_cmd: NotificationCommand) -> None:
|
||||
if old_cmd.desktop_notification_id != new_cmd.desktop_notification_id:
|
||||
self.in_progress_notification_commands.pop(old_cmd.desktop_notification_id, None)
|
||||
if old_cmd.on_update is not None:
|
||||
try:
|
||||
old_cmd.on_update(old_cmd, new_cmd)
|
||||
except Exception as e:
|
||||
self.log('Notification on_update handler failed with error:', e)
|
||||
|
||||
def notification_closed(self, desktop_notification_id: int) -> None:
|
||||
if n := self.in_progress_notification_commands.get(desktop_notification_id):
|
||||
self.purge_notification(n)
|
||||
|
|
@ -704,8 +729,14 @@ def notify_with_command(self, cmd: NotificationCommand, channel_id: int) -> Opti
|
|||
cmd.finalise()
|
||||
if not cmd.title or not self.is_notification_allowed(cmd, channel_id) or self.is_notification_filtered(cmd):
|
||||
return None
|
||||
desktop_notification_id = self.desktop_integration.notify(cmd)
|
||||
existing_desktop_notification_id: Optional[int] = None
|
||||
existing_cmd = self.in_progress_notification_commands_by_client_id.get(cmd.identifier) if cmd.identifier else None
|
||||
if existing_cmd:
|
||||
existing_desktop_notification_id = existing_cmd.desktop_notification_id
|
||||
desktop_notification_id = self.desktop_integration.notify(cmd, existing_desktop_notification_id)
|
||||
self.register_in_progress_notification(cmd, desktop_notification_id)
|
||||
if existing_cmd:
|
||||
self.notification_replaced(existing_cmd, cmd)
|
||||
return desktop_notification_id
|
||||
|
||||
def register_in_progress_notification(self, cmd: NotificationCommand, desktop_notification_id: int) -> None:
|
||||
|
|
|
|||
|
|
@ -39,10 +39,14 @@ def close_notification(self, desktop_notification_id: int) -> bool:
|
|||
self.notification_manager.notification_closed(desktop_notification_id)
|
||||
return self.close_succeeds
|
||||
|
||||
def notify(self, cmd) -> int:
|
||||
self.counter += 1
|
||||
def notify(self, cmd, existing_desktop_notification_id) -> int:
|
||||
if existing_desktop_notification_id:
|
||||
did = existing_desktop_notification_id
|
||||
else:
|
||||
self.counter += 1
|
||||
did = self.counter
|
||||
title, body, urgency = cmd.title, cmd.body, (Urgency.Normal if cmd.urgency is None else cmd.urgency)
|
||||
ans = n(title, body, urgency, self.counter, cmd.icon_name, os.path.basename(cmd.icon_path), cmd.application_name, cmd.notification_type)
|
||||
ans = n(title, body, urgency, did, cmd.icon_name, os.path.basename(cmd.icon_path), cmd.application_name, cmd.notification_type)
|
||||
self.notifications.append(ans)
|
||||
return self.counter
|
||||
|
||||
|
|
@ -275,9 +279,9 @@ def send_with_icon(data='', n='', g=''):
|
|||
send_with_icon(g='gid', data='1')
|
||||
self.ae(di.notifications, [n(icon_path=dc.hash(b'1'))])
|
||||
send_with_icon(g='gid', n='moose')
|
||||
self.ae(di.notifications[-1], n(icon_name='moose', icon_path=dc.hash(b'1'), desktop_notification_id=len(di.notifications)))
|
||||
self.ae(di.notifications[-1], n(icon_name='moose', icon_path=dc.hash(b'1')))
|
||||
send_with_icon(g='gid2', data='2')
|
||||
self.ae(di.notifications[-1], n(icon_path=dc.hash(b'2'), desktop_notification_id=len(di.notifications)))
|
||||
self.ae(di.notifications[-1], n(icon_path=dc.hash(b'2')))
|
||||
reset()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue