Implement a non-functional push rule control UI

This commit is contained in:
miruka 2020-10-30 14:15:17 -04:00
parent 97f6acbb0d
commit bb8f394b78
12 changed files with 364 additions and 11 deletions

View File

@ -4,15 +4,20 @@
- PCN error handling - PCN error handling
- Change docs linking to dev branch back to master - Change docs linking to dev branch back to master
- fix DeviceSection padding
- Implement fallback QML notifications, usable if dbus isn't available - Implement fallback QML notifications, usable if dbus isn't available
- annoying tooltips when menu open - annoying tooltips when menu open
- profiles missing in notifications - profiles missing in notifications
- add http_proxy support - 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 - Encrypted rooms don't show invites in member list after Mirage restart
- Room display name not updated when someone removes theirs - Room display name not updated when someone removes theirs
- Fix right margin of own `<image url>\n<image url>` messages - Fix right margin of own `<image url>\n<image url>` messages
- option to use plaintext notifications
- warn on ambiguously activated shortcut
- SSO device delete?
- filter > enter > room list is always scrolled to top - filter > enter > room list is always scrolled to top
- session list: prevent tab-focusing the delegates - session list: prevent tab-focusing the delegates
- refresh server list button - refresh server list button

View File

@ -66,6 +66,9 @@ class Backend:
- `"accounts"`: logged-in accounts; - `"accounts"`: logged-in accounts;
- `("<user_id>", "pushrules")`: push rules configured for our
account `user_id`.
- `("<user_id>", "rooms")`: rooms our account `user_id` is part of; - `("<user_id>", "rooms")`: rooms our account `user_id` is part of;
- `("<user_id>", "transfers")`: ongoing or failed file - `("<user_id>", "transfers")`: ongoing or failed file

View File

@ -85,7 +85,51 @@ class Account(ModelItem):
return (self.order, self.id) < (other.order, other.id) return (self.order, self.id) < (other.order, other.id)
class PushRuleKind(AutoStrEnum):
Override = auto()
Content = auto()
Room = auto()
Sender = auto()
Underride = auto()
@dataclass(eq=False) @dataclass(eq=False)
class PushRule(ModelItem):
"""A push rule configured for one of our account."""
id: str = field()
kind: PushRuleKind = field()
order: int = field()
default: bool = field()
enabled: bool = True
pattern: str = ""
notify: bool = False
highlight: bool = False
bubble: bool = False
sound: bool = False
urgency_hint: bool = False
def __lt__(self, other: "PushRule") -> bool:
"""Sort by `kind`, then `order`."""
return (
self.kind is PushRuleKind.Underride,
self.kind is PushRuleKind.Sender,
self.kind is PushRuleKind.Room,
self.kind is PushRuleKind.Content,
self.kind is PushRuleKind.Override,
self.order,
) < (
other.kind is PushRuleKind.Underride,
other.kind is PushRuleKind.Sender,
other.kind is PushRuleKind.Room,
other.kind is PushRuleKind.Content,
other.kind is PushRuleKind.Override,
other.order,
)
@dataclass
class Room(ModelItem): class Room(ModelItem):
"""A matrix room we are invited to, are or were member of.""" """A matrix room we are invited to, are or were member of."""

View File

@ -15,7 +15,7 @@ import nio
from .html_markdown import HTML_PROCESSOR from .html_markdown import HTML_PROCESSOR
from .media_cache import Media from .media_cache import Media
from .models.items import TypeSpecifier from .models.items import PushRule, PushRuleKind, TypeSpecifier
from .presence import Presence from .presence import Presence
from .pyotherside_events import DevicesUpdated from .pyotherside_events import DevicesUpdated
from .utils import classes_defined_in, plain2html from .utils import classes_defined_in, plain2html
@ -53,22 +53,23 @@ class NioCallbacks:
if method: if method:
self.client.add_response_callback(method, response_class) self.client.add_response_callback(method, response_class)
for name, event_class in classes_defined_in(nio.events).items(): for name, ev_class in classes_defined_in(nio.events).items():
method = getattr(self, f"on{name}", None) method = getattr(self, f"on{name}", None)
if not method: if not method:
continue continue
if issubclass(event_class, nio.EphemeralEvent): if issubclass(ev_class, nio.EphemeralEvent):
self.client.add_ephemeral_callback(method, event_class) self.client.add_ephemeral_callback(method, ev_class)
elif issubclass(event_class, nio.ToDeviceEvent): elif issubclass(ev_class, nio.ToDeviceEvent):
self.client.add_to_device_callback(method, event_class) self.client.add_to_device_callback(method, ev_class)
elif issubclass(event_class, nio.AccountDataEvent): elif issubclass(ev_class, nio.AccountDataEvent):
self.client.add_room_account_data_callback(method, event_class) self.client.add_global_account_data_callback(method, ev_class)
elif issubclass(event_class, nio.PresenceEvent): self.client.add_room_account_data_callback(method, ev_class)
self.client.add_presence_callback(method, event_class) elif issubclass(ev_class, nio.PresenceEvent):
self.client.add_presence_callback(method, ev_class)
else: else:
self.client.add_event_callback(method, event_class) self.client.add_event_callback(method, ev_class)
@property @property
@ -779,6 +780,54 @@ class NioCallbacks:
ev.read_by_count = len(ev.last_read_by) ev.read_by_count = len(ev.last_read_by)
# Account data callbacks
async def onPushRulesEvent(self, ev: nio.PushRulesEvent) -> None:
model = self.models[self.user_id, "pushrules"]
model.clear()
kinds: Dict[PushRuleKind, List[nio.PushRule]] = {
PushRuleKind.Override: ev.global_rules.override,
PushRuleKind.Content: ev.global_rules.content,
PushRuleKind.Room: ev.global_rules.room,
PushRuleKind.Sender: ev.global_rules.sender,
PushRuleKind.Underride: ev.global_rules.underride,
}
for kind, rules in kinds.items():
for order, rule in enumerate(rules):
tweaks = {
action.tweak: action.value for action in rule.actions
if isinstance(action, nio.PushSetTweak)
}
# Note: The `dont_notify` action does nothing.
# As of now (sept 2020), `coalesce` is just a `notify` synonym.
notify = any(
isinstance(action, (nio.PushNotify, nio.PushCoalesce))
for action in rule.actions
)
high = tweaks.get("highlight", False) is not False
bubble = tweaks.get("bubble", notify) is not False
sound = tweaks.get("sound", False) is not False
hint = tweaks.get("urgency_hint", high) is not False
model[rule.id] = PushRule(
id = rule.id,
kind = kind,
order = order,
default = rule.default,
enabled = rule.enabled,
pattern = rule.pattern,
notify = notify,
highlight = high,
bubble = bubble,
sound = sound,
urgency_hint = hint,
)
# Presence event callbacks # Presence event callbacks
async def onPresenceEvent(self, ev: nio.PresenceEvent) -> None: async def onPresenceEvent(self, ev: nio.PresenceEvent) -> None:

View File

@ -18,11 +18,14 @@ HPage {
height: Math.min(implicitHeight, page.availableHeight) height: Math.min(implicitHeight, page.availableHeight)
header: HTabBar { header: HTabBar {
currentIndex: 1 // XXX
HTabButton { text: qsTr("Account") } HTabButton { text: qsTr("Account") }
HTabButton { text: qsTr("Notifications") }
HTabButton { text: qsTr("Security") } HTabButton { text: qsTr("Security") }
} }
Account { userId: page.userId } Account { userId: page.userId }
Notifications { userId: page.userId }
Security { userId: page.userId } Security { userId: page.userId }
} }
} }

View File

@ -0,0 +1,12 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import "../../Base"
HButton {
property bool on: true
opacity: on ? 1 : theme.disabledElementsOpacity
hoverEnabled: true
backgroundColor: "transparent"
}

View File

@ -0,0 +1,157 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import "../.."
import "../../Base"
import "../../Base/Buttons"
import "../../Base/HTile"
import "../../MainPane"
import "../../PythonBridge"
import "../../ShortcutBundles"
HTile {
id: root
property string userId
readonly property QtObject matchingRoom:
model.kind === "Room" ?
ModelStore.get(userId, "rooms").find(model.id) :
null
contentOpacity: model.enabled ? 1 : theme.disabledElementsOpacity
hoverEnabled: false
contentItem: HColumnLayout {
spacing: root.spacing / 2
TitleLabel {
opacity: model.enabled ? 1 : theme.disabledElementsOpacity
elide: Text.ElideNone
wrapMode: HLabel.Wrap
textFormat:
model.id === ".m.rule.contains_user_name" ||
model.id === ".m.rule.roomnotif" ||
model.kind === "Sender" ?
HLabel.StyledText :
HLabel.PlainText
text:
model.id === ".m.rule.master" ?
qsTr("Any message") :
model.id === ".m.rule.suppress_notices" ?
qsTr("Messages sent by bots") :
model.id === ".m.rule.invite_for_me" ?
qsTr("Received room invites") :
model.id === ".m.rule.member_event" ?
qsTr("Membership, name & avatar changes") :
model.id === ".m.rule.contains_display_name" ?
qsTr("Messages containing my display name") :
model.id === ".m.rule.tombstone" ?
qsTr("Room migration alerts") :
model.id === ".m.rule.reaction" ?
qsTr("Emoji reactions") :
model.id === ".m.rule.roomnotif" ?
qsTr("Messages containing %1").arg(
utils.htmlColorize("@room", theme.colors.accentText),
) :
model.id === ".m.rule.contains_user_name" ?
qsTr("Contains %1").arg(utils.coloredNameHtml(
"", userId, userId.split(":")[0].substring(1),
)):
model.id === ".m.rule.call" ?
qsTr("Incoming audio calls") :
model.id === ".m.rule.encrypted_room_one_to_one" ?
qsTr("Encrypted 1-to-1 messages") :
model.id === ".m.rule.room_one_to_one" ?
qsTr("Unencrypted 1-to-1 messages") :
model.id === ".m.rule.message" ?
qsTr("Unencrypted group messages") :
model.id === ".m.rule.encrypted" ?
qsTr("Encrypted group messages") :
model.kind === "Content" ?
qsTr('Contains "%1"').arg(model.pattern) :
model.kind === "Sender" ?
utils.coloredNameHtml("", model.id) :
matchingRoom && matchingRoom.display_name ?
matchingRoom.display_name :
model.id
Layout.fillWidth: true
}
HRowLayout {
NotificationRuleButton {
on: model.notify
contentItem: MessageIndicator {
indicatorTheme:
theme.mainPane.listView.room.unreadIndicator
unreads: 1
text: "+1"
font.pixelSize: theme.fontSize.normal
topPadding: leftPadding / 3
bottomPadding: topPadding
}
}
NotificationRuleButton {
on: model.highlight
contentItem: MessageIndicator {
indicatorTheme:
theme.mainPane.listView.room.unreadIndicator
unreads: 1
highlights: 1
text: "+1"
font.pixelSize: theme.fontSize.normal
topPadding: leftPadding / 3
bottomPadding: topPadding
}
}
NotificationRuleButton {
icon.name: "pushrule-action-bubble"
on: model.bubble
}
NotificationRuleButton {
icon.name: "pushrule-action-sound"
on: model.sound
}
NotificationRuleButton {
icon.name: "pushrule-action-urgency-hint"
on: model.urgency_hint
}
HSpacer {}
NotificationRuleButton {
icon.name: "pushrule-edit"
}
}
}
}

View File

@ -0,0 +1,56 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import "../.."
import "../../Base"
import "../../Base/Buttons"
import "../../PythonBridge"
import "../../ShortcutBundles"
HListView {
id: root
property string userId
property bool enableFlickShortcuts:
SwipeView ? SwipeView.isCurrentItem : true
function takeFocus() {
// deviceList.headerItem.exportButton.forceActiveFocus()
}
clip: true
model: ModelStore.get(userId, "pushrules")
bottomMargin: theme.spacing
implicitHeight: Math.min(window.height, contentHeight + bottomMargin)
section.property: "kind"
section.delegate: HLabel {
width: root.width
topPadding: padding * (section === "Override" ? 1 : 1.5)
padding: theme.spacing
font.pixelSize: theme.fontSize.big
text:
section === "Override" ? qsTr("High-priority general rules") :
section === "Content" ? qsTr("Message text rules") :
section === "Room" ? qsTr("Room rules") :
section === "Sender" ? qsTr("Sender rules") :
qsTr("General rules")
}
delegate: NotificationRuleDelegate {
userId: root.userId
width: root.width
}
Layout.fillWidth: true
Layout.fillHeight: true
FlickShortcuts {
flickable: root
active: ! mainUI.debugConsole.visible && root.enableFlickShortcuts
}
}

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m22 3v13h-6.961l-3.039 3.798-3.039-3.798h-6.961v-13zm2-2h-24v17h8l4 5 4-5h8z"/>
</svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@ -0,0 +1,11 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m14.509208 8.343398c1.202865.9346638 1.945334 2.226588 1.94209 3.651856-.0032 1.425268-.748954 2.714677-1.953439 3.648082l.978322 1.538485c1.713516-1.324631 2.775344-3.158736 2.780206-5.184051.0032-2.0265727-1.05048-3.8594193-2.759132-5.1890826z" stroke-width="1.428038"/>
<path d="m18.318701 4.9920248c2.307024 1.7957761 3.72921 4.2712232 3.723838 7.0065862-.0054 2.735361-1.440099 5.212198-3.754288 7.003804l.980846 1.74296c2.887361-2.234991 4.68032-5.328952 4.685693-8.743986.009-3.4150319-1.766089-6.5062134-4.648077-8.7467639z" stroke-width="1.57784"/>
<g transform="matrix(.6875 0 0 1.01875 0 -.225)">
<path d="m11 1.8125-9.1191406 5.09375v10.1875l9.1191406 5.09375zm-1.6875 2.9941406v14.3867184l-7.4324287-4.041727v-6.1767663z" transform="matrix(1.4545455 0 0 .98159509 0 .220859)"/>
<path d="m2.7346492 8.9958856h-2.7346492v6.1896554h2.7346492z" stroke-width=".786744"/>
<path d="m7 7c-4.7095572 2.7190641-5.0350591 2.9069927 0 0z" fill="none"/>
<path d="m2.7357957 7-2.7357957 1.0208759v.9750097h2.7346492z"/>
<path d="m2.7357956 17-2.73579573-1.020876v-.97501h2.73464923z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m22.909091 9.2836364v1.4967276h-2.768727c.02182-.243273.04145-.488728.04145-.742909 0-.2585459-.01854-.5061824-.03818-.7527277h2.765455zm-11.616-7.1487273v-2.1349091h1.495636v2.1425455c-.264-.024-.528-.038183-.792-.038183-.234545 0-.469091.010909-.703636.030547zm6.022909 1.6843636 1.712727-1.9014545 1.111637 1.0014544-1.748728 1.9385456c-.272727-.324-.646909-.733091-1.075636-1.0385455zm-11.7141819 1.0385455-1.7487273-1.9385456 1.1116364-1.0014544 1.7127273 1.9014545c-.4276365.3054545-.8029091.7145455-1.0756364 1.0385455zm-1.7421818 5.9225458h-2.7687273v-1.4967276h2.7654545c-.019636.2465453-.038182.4952727-.038182.7538186 0 .254181.019636.499636.041455.742909zm10.3221817 9.946909h-4.363636c-.301091 0-.545455.244363-.545455.545454s.244364.545455.545455.545455h4.363636c.301091 0 .545455-.244364.545455-.545455s-.244364-.545454-.545455-.545454zm0 2.181818h-4.363636c-.301091 0-.545455.244364-.545455.545454 0 .301091.244364.545455.545455.545455h4.363636c.301091 0 .545455-.244364.545455-.545455 0-.30109-.244364-.545454-.545455-.545454zm4.363636-12.871636c0 3.893453-3.506181 6.526909-3.506181 9.598909h-2.169819c-.0033-2.026909.949091-3.697091 1.877455-5.309455.830182-1.445454 1.616727-2.811273 1.616727-4.289454 0-2.8276368-2.263636-4.1149096-4.366909-4.1149096-2.1 0-4.3603635 1.2872728-4.3603635 4.1149096 0 1.478181.7865455 2.844 1.6167275 4.289454.928363 1.612364 1.881818 3.282546 1.876363 5.309455h-2.1687268c0-3.072-3.5061818-5.705456-3.5061818-9.598909 0-4.0614551 3.2705454-6.2967278 6.5421816-6.2967278 3.273818 0 6.548727 2.2374545 6.548727 6.2967278z" stroke-width="1.09091"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,7 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<g stroke-width=".853763" transform="translate(9.811836)">
<path d="m12 0c1.208596 0 2.188164.97956763 2.188164 2.1881632 0 1.2085956-.979568 2.1881633-2.188164 2.1881633s-2.1881636-.9795677-2.1881636-2.1881633c0-1.20859557.9795676-2.1881632 2.1881636-2.1881632z"/>
<path d="m12 9.8118368c1.208596 0 2.188164.9795672 2.188164 2.1881632s-.979568 2.188163-2.188164 2.188163-2.1881636-.979567-2.1881636-2.188163.9795676-2.1881632 2.1881636-2.1881632z"/>
<path d="m12 19.623674c1.208596 0 2.188164.979567 2.188164 2.188163s-.979568 2.188163-2.188164 2.188163-2.1881636-.979567-2.1881636-2.188163.9795676-2.188163 2.1881636-2.188163z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 760 B