From 3dd12691b8bd29a41dd72ca7bf3d51cfcea20d03 Mon Sep 17 00:00:00 2001 From: miruka Date: Tue, 3 Nov 2020 07:29:32 -0400 Subject: [PATCH] Make desktop notifications follow push rules --- docs/TODO.md | 9 ++- src/backend/matrix_client.py | 84 ++++++++++++-------------- src/backend/nio_callbacks.py | 2 + src/backend/pyotherside_events.py | 21 ++++--- src/gui/PythonBridge/EventHandlers.qml | 25 +++++--- 5 files changed, 78 insertions(+), 63 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index c5a152b3..bc2be6b9 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -4,17 +4,22 @@ - PCN error handling - Change docs linking to dev branch back to master +- Implement fallback QML notifications, usable if dbus isn't available +- profiles missing in notifications +- option to use plaintext notifications +- Notification urgency level (plyer)? +- Rename the alertOn(Mention/Message)ForMsec options, default Message to non-0 +- Make local unread counter an optional turned off by default + - fix DeviceSection padding - Implement fallback QML notifications, usable if dbus isn't available - annoying tooltips when menu open -- profiles missing in notifications - add http_proxy support - image viewer: can't expand image in reduced window layout - Encrypted rooms don't show invites in member list after Mirage restart - Room display name not updated when someone removes theirs - Fix right margin of own `\n` messages -- option to use plaintext notifications - warn on ambiguously activated shortcut - SSO device delete? diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index 1223007b..a2e89064 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -209,8 +209,7 @@ class MatrixClient(nio.AsyncClient): self.unassigned_event_last_read_by: DefaultDict[str, Dict[str, int]] =\ DefaultDict(dict) - self.previous_server_unreads: Dict[str, int] = {} - self.previous_server_highlights: Dict[str, int] = {} + self.push_rules: nio.PushRulesEvent = nio.PushRulesEvent() # {room_id: event} self.power_level_events: Dict[str, nio.PowerLevelsEvent] = {} @@ -2247,55 +2246,52 @@ class MatrixClient(nio.AsyncClient): await self.update_account_unread_counts() return item + self.models[self.user_id, "rooms"][room.room_id].local_unreads = True + await self.update_account_unread_counts() + # Alerts & notifications - room_item = self.models[self.user_id, "rooms"][room.room_id] + name = self.models["accounts"][self.user_id].display_name + nio_rule = self.push_rules.global_rules.matching_rule(ev, room, name) - unread = \ - room_item.unreads and \ - room_item.unreads != \ - self.previous_server_unreads.get(room.room_id, 0) + if not nio_rule: + return item - highlight = \ - room_item.highlights and \ - room_item.highlights != \ - self.previous_server_highlights.get(room.room_id, 0) + model = self.models[self.user_id, "pushrules"] + rule = model[nio_rule.kind.value, nio_rule.id] - self.previous_server_unreads[room.room_id] = room_item.unreads - self.previous_server_highlights[room.room_id] = room_item.highlights + if not rule.notify and not rule.highlight: + return item - room_item.local_unreads = True + sender = item.sender_name or item.sender_id - if unread or highlight: - members = self.models[self.user_id, room.room_id, "members"] - room_name = room.display_name - sender = item.sender_name or item.sender_id - - if isinstance(ev, nio.RoomMessageEmote): - body = f"{sender} {item.inline_content}" - elif not isinstance(ev, nio.RoomMessage): - body = item.inline_content.replace( - "%1", item.sender_name or item.sender_id, - ).replace( - "%2", item.target_name or item.target_id, - ) - elif len(members) == 2 and room_name == sender: - body = item.inline_content - else: - body = f"{sender}: {item.inline_content}" - - NotificationRequested( - id = item.id, - high_importance = highlight, - title = room_name, - - body = body.replace(" ⏎ ", "
") - .replace(" ⏎⏎ ", f"
{'─' * 24}
"), - - image = await self.get_notification_avatar( - mxc=item.sender_avatar, user_id=item.sender_id, - ) if item.sender_avatar else "", + if isinstance(ev, nio.RoomMessageEmote): + body = f"{sender} {item.inline_content}" + elif not isinstance(ev, nio.RoomMessage): + body = item.inline_content.replace( + "%1", item.sender_name or item.sender_id, + ).replace( + "%2", item.target_name or item.target_id, ) + elif room.member_count == 2 and room.display_name == sender: + body = item.inline_content + else: + body = f"{sender}: {item.inline_content}" + + NotificationRequested( + id = item.id, + critical = rule.highlight, + bubble = rule.bubble, + sound = rule.sound, + urgency_hint = rule.urgency_hint, + + title = room.display_name, + body = body.replace(" ⏎ ", "
") + .replace(" ⏎⏎ ", f"
{'─' * 24}
"), + + image = await self.get_notification_avatar( + mxc=item.sender_avatar, user_id=item.sender_id, + ) if item.sender_avatar else "", + ) - await self.update_account_unread_counts() return item diff --git a/src/backend/nio_callbacks.py b/src/backend/nio_callbacks.py index 3995bbdf..fd9f3768 100644 --- a/src/backend/nio_callbacks.py +++ b/src/backend/nio_callbacks.py @@ -839,6 +839,8 @@ class NioCallbacks: urgency_hint = hint, ) + self.client.push_rules = ev + # Presence event callbacks diff --git a/src/backend/pyotherside_events.py b/src/backend/pyotherside_events.py index 98f067fc..30392293 100644 --- a/src/backend/pyotherside_events.py +++ b/src/backend/pyotherside_events.py @@ -31,17 +31,22 @@ class PyOtherSideEvent: @dataclass class NotificationRequested(PyOtherSideEvent): - """Request a notification and window manager alert to be shown. + """Request a notification bubble, sound or window urgency hint. - Alerts set the urgency hint for compliant X11/Wayland window managers or - flash the program's taskbar icon on Windows. + Urgency hints usually flash or highlight the program's icon in a taskbar, + dock or panel. """ - id: str = field() - title: str = field() - body: str = "" - image: Union[Path, str] = "" - high_importance: bool = False + id: str = field() + critical: bool = False + bubble: bool = False + sound: bool = False + urgency_hint: bool = False + + # Bubble parameters + title: str = "" + body: str = "" + image: Union[Path, str] = "" @dataclass diff --git a/src/gui/PythonBridge/EventHandlers.qml b/src/gui/PythonBridge/EventHandlers.qml index dbdfb9ea..2ea1325b 100644 --- a/src/gui/PythonBridge/EventHandlers.qml +++ b/src/gui/PythonBridge/EventHandlers.qml @@ -8,26 +8,33 @@ import ".." QtObject { signal deviceUpdateSignal(string forAccount) - function onNotificationRequested(id, title, body, image, highImportance) { + function onNotificationRequested( + id, critical, bubble, sound, urgencyHint, title, body, image, + ) { const level = window.notificationLevel if (level === Window.NotificationLevel.None) return - if (level === Window.MentionsKeywords && ! highImportance) return + if (level === Window.MentionsKeywords && ! critical) return if (window.notifiedIds.has(id)) return window.notifiedIds.add(id) window.notifiedIdsChanged() - if (Qt.application.state === Qt.ApplicationActive) return + if (Qt.application.state === Qt.ApplicationActive) + return - py.callCoro("desktop_notify", [title, body, image]) + if (bubble) + py.callCoro("desktop_notify", [title, body, image]) - const msec = - highImportance ? - window.settings.Notifications.urgent_alert_time * 1000 : - window.settings.Notifications.alert_time * 1000 + if (urgencyHint) { + const msec = + critical ? + window.settings.Notifications.urgent_alert_time * 1000 : + window.settings.Notifications.alert_time * 1000 - if (msec) window.alert(msec === -1 ? 0 : msec) // -1 → 0 = no time out + // -1 ? 0 for no time out : msec + if (msec !== 0) window.alert(msec === -1 ? 0 : msec) + } } function onCoroutineDone(uuid, result, error, traceback) {