Add popup to edit push rules

This commit is contained in:
miruka 2021-02-22 11:32:34 -04:00
parent 71cd509a9d
commit 765ce46aeb
24 changed files with 919 additions and 92 deletions

View File

@ -1,5 +1,21 @@
# TODO # TODO
- push popup cancel & remove
- right click on rule
- combo box custom item
- explain pattern
- fix spinbox buttons
- way to add new rule
- quick room & sender rule changes
- config & keybind for global rule disabling
- quick settings
- import/export/json edit rules?
- add missing license headers to qml files
- fix flickable popups can't be flicked by keyboard
- room selector for room rules
- validate json for unknown action/condition
- seen tooltips can't be shown on image hover
- PCN docstrings - PCN docstrings
- PCN error handling - PCN error handling
- Change docs linking to dev branch back to master - Change docs linking to dev branch back to master

View File

@ -41,8 +41,8 @@ from .errors import (
from .html_markdown import HTML_PROCESSOR as HTML from .html_markdown import HTML_PROCESSOR as HTML
from .media_cache import Media, Thumbnail from .media_cache import Media, Thumbnail
from .models.items import ( from .models.items import (
ZERO_DATE, Account, Event, Member, PushRule, Room, ZERO_DATE, Account, Event, Member, PushRule, Room, Transfer,
Transfer, TransferStatus, TypeSpecifier, TransferStatus, TypeSpecifier,
) )
from .models.model_store import ModelStore from .models.model_store import ModelStore
from .nio_callbacks import NioCallbacks from .nio_callbacks import NioCallbacks
@ -54,8 +54,10 @@ from .pyotherside_events import (
if TYPE_CHECKING: if TYPE_CHECKING:
from .backend import Backend from .backend import Backend
CryptDict = Dict[str, Any] PushAction = Union[Dict[str, Any], nio.PushAction]
PathCallable = Union[ PushCondition = Union[Dict[str, Any], nio.PushCondition]
CryptDict = Dict[str, Any]
PathCallable = Union[
str, Path, Callable[[], Coroutine[None, None, Union[str, Path]]], str, Path, Callable[[], Coroutine[None, None, Union[str, Path]]],
] ]
@ -1780,6 +1782,79 @@ class MatrixClient(nio.AsyncClient):
raise MatrixUnauthorized() raise MatrixUnauthorized()
async def edit_pushrule(
self,
kind: Union[nio.PushRuleKind, str],
rule_id: str,
old_kind: Union[None, nio.PushRuleKind, str] = None,
old_rule_id: Optional[str] = None,
move_before_rule_id: Optional[str] = None,
move_after_rule_id: Optional[str] = None,
enable: Optional[bool] = None,
conditions: Optional[List[PushCondition]] = None,
pattern: Optional[str] = None,
actions: Optional[List[PushAction]] = None,
) -> None:
"""Create or edit an existing non-builtin pushrule."""
# Convert arguments that were passed as basic types (usually from QML)
if isinstance(old_kind, str):
old_kind = nio.PushRuleKind[old_kind]
kind = nio.PushRuleKind[kind] if isinstance(kind, str) else kind
conditions = [
nio.PushCondition.from_dict(c) if isinstance(c, dict) else c
for c in conditions
] if isinstance(conditions, list) else None
actions = [
nio.PushAction.from_dict(a) if isinstance(a, dict) else a
for a in actions
] if isinstance(actions, list) else None
# Now edit the rule
old: Optional[PushRule] = None
key = (old_kind.value, old_rule_id)
if None not in key:
old = self.models[self.user_id, "pushrules"].get(key)
kind_change = old_kind is not None and old_kind != kind
rule_id_change = old_rule_id is not None and old_rule_id != rule_id
explicit_move = move_before_rule_id or move_after_rule_id
if old and not kind_change and not explicit_move:
# If user edits a rule without specifying a new position,
# the server would move it to the first position
move_after_rule_id = old.rule_id
if old and actions is None:
# Matrix API forces us to always pass a non-null actions paramater
actions = [nio.PushAction.from_dict(a) for a in old.actions]
await self.set_pushrule(
scope = "global",
kind = kind,
rule_id = rule_id,
before = move_before_rule_id,
after = move_after_rule_id,
actions = actions,
conditions = conditions,
pattern = pattern,
)
# If we're editing an existing rule but its kind or ID is changed,
# set_pushrule creates a new rule, thus we must delete the old one
if kind_change or rule_id_change:
await self.delete_pushrule("global", old_kind, old_rule_id)
if enable is not None and (old.enabled if old else True) != enable:
await self.enable_pushrule("global", kind, rule_id, enable)
async def tweak_pushrule( async def tweak_pushrule(
self, self,
kind: Union[nio.PushRuleKind, str], kind: Union[nio.PushRuleKind, str],
@ -1790,6 +1865,7 @@ class MatrixClient(nio.AsyncClient):
sound: Optional[str] = None, sound: Optional[str] = None,
urgency_hint: Optional[bool] = None, urgency_hint: Optional[bool] = None,
) -> None: ) -> None:
"""Set an existing pushrule's actions. Works for builtin rules."""
kind = nio.PushRuleKind[kind] if isinstance(kind, str) else kind kind = nio.PushRuleKind[kind] if isinstance(kind, str) else kind

View File

@ -88,18 +88,20 @@ class Account(ModelItem):
class PushRule(ModelItem): class PushRule(ModelItem):
"""A push rule configured for one of our account.""" """A push rule configured for one of our account."""
id: Tuple[str, str] = field() # (kind.value, rule_id) id: Tuple[str, str] = field() # (kind.value, rule_id)
kind: nio.PushRuleKind = field() kind: nio.PushRuleKind = field()
rule_id: str = field() rule_id: str = field()
order: int = field() order: int = field()
default: bool = field() default: bool = field()
enabled: bool = True enabled: bool = True
pattern: str = "" conditions: List[Dict[str, Any]] = field(default_factory=list)
notify: bool = False pattern: str = ""
highlight: bool = False actions: List[Dict[str, Any]] = field(default_factory=list)
bubble: bool = False notify: bool = False
sound: str = "" # usually "default" when set highlight: bool = False
urgency_hint: bool = False bubble: bool = False
sound: str = "" # usually "default" when set
urgency_hint: bool = False
def __lt__(self, other: "PushRule") -> bool: def __lt__(self, other: "PushRule") -> bool:
"""Sort by `kind`, then `order`.""" """Sort by `kind`, then `order`."""

View File

@ -4,7 +4,7 @@
import asyncio import asyncio
import json import json
import logging as log import logging as log
from dataclasses import dataclass, field from dataclasses import asdict, dataclass, field
from datetime import datetime, timedelta from datetime import datetime, timedelta
from html import escape from html import escape
from pathlib import Path from pathlib import Path
@ -786,7 +786,8 @@ class NioCallbacks:
model = self.models[self.user_id, "pushrules"] model = self.models[self.user_id, "pushrules"]
kinds: Dict[nio.PushRuleKind, List[nio.PushRule]] = { kinds: Dict[nio.PushRuleKind, List[nio.PushRule]] = {
kind: getattr(ev.global_rules, kind.value) for kind in nio.PushRuleKind kind: getattr(ev.global_rules, kind.value)
for kind in nio.PushRuleKind
} }
# Remove from model rules that are now deleted. # Remove from model rules that are now deleted.
@ -831,7 +832,9 @@ class NioCallbacks:
order = order, order = order,
default = rule.default, default = rule.default,
enabled = rule.enabled, enabled = rule.enabled,
conditions = [c.as_value for c in rule.conditions],
pattern = rule.pattern, pattern = rule.pattern,
actions = [a.as_value for a in rule.actions],
notify = notify, notify = notify,
highlight = high, highlight = high,
bubble = bubble, bubble = bubble,

71
src/gui/Base/HSpinBox.qml Normal file
View File

@ -0,0 +1,71 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.12
SpinBox {
id: box
property var defaultValue: null
readonly property bool changed: value !== (defaultValue || 0)
function reset() { value = Qt.binding(() => defaultValue || 0) }
// XXX TODO: default binding break
value: defaultValue || 0
implicitHeight: theme.baseElementsHeight
padding: 0
editable: true
background: null
contentItem: HTextField {
id: textField
height: parent.height
implicitWidth: 90 * theme.uiScale
radius: 0
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
// FIXME
text: box.textFromValue(box.value, box.locale)
readOnly: ! box.editable
validator: box.validator
inputMethodHints: Qt.ImhFormattedNumbersOnly
onTextChanged: if (text && text !== "-") box.value = text
}
down.indicator: HButton {
x: box.mirrored ? parent.width - width : 0
height: parent.height
font.pixelSize: theme.fontSize.biggest
text: qsTr("-")
autoRepeat: true
autoRepeatInterval: 50
onPressed: box.decrease()
}
up.indicator: HButton {
x: box.mirrored ? 0 : parent.width - width
height: parent.height
font.pixelSize: theme.fontSize.biggest
text: qsTr("+")
autoRepeat: true
autoRepeatInterval: 50
onPressed: box.increase()
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: textField.hovered ? Qt.IBeamCursor : Qt.ArrowCursor
onWheel: wheel => {
wheel.angleDelta.y < 0 ? box.decrease() : box.increase()
wheel.accepted()
}
}
}

View File

@ -1,3 +1,4 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12 import QtQuick 2.12
@ -5,19 +6,15 @@ import QtQuick.Layouts 1.12
import "../.." import "../.."
import "../../Base" import "../../Base"
import "../../Base/HTile" import "../../Base/HTile"
import "../../Base/Buttons"
import "../../MainPane" import "../../MainPane"
import "../../Popups"
HTile { HTile {
id: root id: root
property Item page property Item page
readonly property QtObject matchingRoom:
model.kind === "room" ?
ModelStore.get(page.userId, "rooms").find(model.rule_id) :
null
contentOpacity: model.enabled ? 1 : theme.disabledElementsOpacity contentOpacity: model.enabled ? 1 : theme.disabledElementsOpacity
hoverEnabled: false hoverEnabled: false
leftPadding: theme.spacing / 4 leftPadding: theme.spacing / 4
@ -30,71 +27,8 @@ HTile {
opacity: model.enabled ? 1 : theme.disabledElementsOpacity opacity: model.enabled ? 1 : theme.disabledElementsOpacity
elide: Text.ElideNone elide: Text.ElideNone
wrapMode: HLabel.Wrap wrapMode: HLabel.Wrap
textFormat: HLabel.StyledText
textFormat: text: utils.formatPushRuleName(page.userId, model)
model.rule_id === ".m.rule.contains_user_name" ||
model.rule_id === ".m.rule.roomnotif" ||
model.kind === "sender" ?
HLabel.StyledText :
HLabel.PlainText
text:
model.rule_id === ".m.rule.master" ?
qsTr("Any message") :
model.rule_id === ".m.rule.suppress_notices" ?
qsTr("Messages sent by bots") :
model.rule_id === ".m.rule.invite_for_me" ?
qsTr("Received room invites") :
model.rule_id === ".m.rule.member_event" ?
qsTr("Membership, name & avatar changes") :
model.rule_id === ".m.rule.contains_display_name" ?
qsTr("Messages containing my display name") :
model.rule_id === ".m.rule.tombstone" ?
qsTr("Room migration alerts") :
model.rule_id === ".m.rule.reaction" ?
qsTr("Emoji reactions") :
model.rule_id === ".m.rule.roomnotif" ?
qsTr("Messages containing %1").arg(
utils.htmlColorize("@room", theme.colors.accentText),
) :
model.rule_id === ".m.rule.contains_user_name" ?
qsTr("Contains %1").arg(utils.coloredNameHtml(
"", page.userId, page.userId.split(":")[0].substring(1),
)):
model.rule_id === ".m.rule.call" ?
qsTr("Incoming audio calls") :
model.rule_id === ".m.rule.encrypted_room_one_to_one" ?
qsTr("Encrypted 1-to-1 messages") :
model.rule_id === ".m.rule.room_one_to_one" ?
qsTr("Unencrypted 1-to-1 messages") :
model.rule_id === ".m.rule.message" ?
qsTr("Unencrypted group messages") :
model.rule_id === ".m.rule.encrypted" ?
qsTr("Encrypted group messages") :
model.kind === "content" ?
qsTr('Contains "%1"').arg(model.pattern) :
model.kind === "sender" ?
utils.coloredNameHtml("", model.rule_id) :
matchingRoom && matchingRoom.display_name ?
matchingRoom.display_name :
model.rule_id
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: theme.spacing Layout.leftMargin: theme.spacing
@ -161,6 +95,10 @@ HTile {
NotificationRuleButton { NotificationRuleButton {
icon.name: "pushrule-edit" icon.name: "pushrule-edit"
onClicked: window.makePopup(
"Popups/PushRuleSettingsPopup/PushRuleSettingsPopup.qml",
{userId: page.userId, rule: model},
)
} }
} }
} }

View File

@ -73,11 +73,11 @@ HListView {
padding: theme.spacing padding: theme.spacing
font.pixelSize: theme.fontSize.big font.pixelSize: theme.fontSize.big
text: text:
section === "override" ? qsTr("High-priority general rules") : section === "override" ? qsTr("High priority general rules") :
section === "content" ? qsTr("Message text rules") : section === "content" ? qsTr("Message content rules") :
section === "room" ? qsTr("Room rules") : section === "room" ? qsTr("Room rules") :
section === "sender" ? qsTr("Sender rules") : section === "sender" ? qsTr("Sender rules") :
qsTr("General rules") qsTr("Low priority general rules")
} }
delegate: NotificationRuleDelegate { delegate: NotificationRuleDelegate {

View File

@ -0,0 +1,20 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
import "../../Base/Buttons"
HColumnLayout {
readonly property alias idField: idField
HLabeledItem {
// TODO: globbing explanation & do space works?
label.text: qsTr("Word:")
Layout.fillWidth: true
HTextField {
id: idField
width: parent.width
defaultText: rule.kind === "content" ? rule.pattern : ""
}
}
}

View File

@ -0,0 +1,14 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
HLabel {
opacity: enabled ? 1 : theme.disabledElementsOpacity
wrapMode: HLabel.Wrap
Layout.fillWidth: true
Behavior on opacity { HNumberAnimation {} }
}

View File

@ -0,0 +1,119 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
import "../../Base/Buttons"
HColumnLayout {
readonly property alias idField: idField
readonly property var matrixConditions: {
const results = []
for (let i = 0; i < conditionRepeater.count; i++) {
results.push(conditionRepeater.itemAt(i).control.matrixObject)
}
return results
}
spacing: theme.spacing / 2
HLabeledItem {
label.text: rule.default ? qsTr("Rule ID:") : qsTr("Rule name:")
Layout.fillWidth: true
HTextField {
id: idField
width: parent.width
defaultText: rule.rule_id
// TODO: minimum length, check no dupe
}
}
HRowLayout {
Layout.topMargin: theme.spacing / 2
CustomLabel {
text: qsTr("Conditions for a message to trigger this rule:")
}
PositiveButton {
icon.name: "pushrule-condition-add"
iconItem.small: true
Layout.fillHeight: true
Layout.fillWidth: false
onClicked: addConditionMenu.open()
HMenu {
id: addConditionMenu
x: -width + parent.width
y: parent.height
HMenuItem {
text: qsTr("Room has a certain number of members")
}
HMenuItem {
text: qsTr("Message property matches value")
}
HMenuItem {
text: qsTr("Message contains my display name")
}
HMenuItem {
text: qsTr(
"Sender has permission to trigger special notification"
)
}
HMenuItem {
text: qsTr("Custom JSON condition")
}
}
}
}
CustomLabel {
text: qsTr("No conditions added, all messages will match")
color: theme.colors.dimText
visible: Layout.preferredHeight > 0
Layout.preferredHeight: conditionRepeater.count ? 0 : implicitHeight
Behavior on Layout.preferredHeight { HNumberAnimation {} }
}
Repeater {
id: conditionRepeater
model: JSON.parse(rule.conditions)
HRowLayout {
readonly property Item control: loader.item
spacing: theme.spacing
HLoader {
id: loader
readonly property var condition: modelData
readonly property string filename:
modelData.kind === "event_match" ?
"PushEventMatch" :
modelData.kind === "contains_display_name" ?
"PushContainsDisplayName" :
modelData.kind === "room_member_count" ?
"PushRoomMemberCount" :
modelData.kind === "sender_notification_permission" ?
"PushSenderNotificationPermission" :
"PushUnknownCondition"
asynchronous: false
source: "PushConditions/" + filename + ".qml"
Layout.fillWidth: true
}
NegativeButton {
icon.name: "pushrule-condition-remove"
iconItem.small: true
Layout.fillHeight: true
Layout.fillWidth: false
}
}
}
}

View File

@ -0,0 +1,11 @@
import QtQuick 2.12
import "../../../Base"
HFlow {
spacing: theme.spacing / 2
// transitions break CustomLabel opacity for some reason
populate: null
add: null
move: null
}

View File

@ -0,0 +1,9 @@
import QtQuick 2.12
import ".."
import "../../../Base"
CustomLabel {
readonly property var matrixObject: ({kind: "contains_display_name"})
text: qsTr("Message contains my display name")
}

View File

@ -0,0 +1,58 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import ".."
import "../../../Base"
CustomFlow {
readonly property var matrixObject: ({
kind: "event_match",
key: keyCombo.editText,
pattern: patternField.text,
})
CustomLabel {
text: qsTr("Message")
verticalAlignment: CustomLabel.AlignVCenter
height: keyCombo.height
}
HComboBox {
id: keyCombo
width: Math.min(implicitWidth, parent.width)
editText: condition.key
editable: true
currentIndex: model.indexOf(condition.key)
model: [...new Set([
"content.body",
"content.msgtype",
"room_id",
"sender",
"state_key",
"type",
condition.key,
])].sort()
}
CustomLabel {
text: keyCombo.editText === "content.body" ? qsTr("has") : qsTr("is")
verticalAlignment: CustomLabel.AlignVCenter
height: keyCombo.height
}
HTextField {
id: patternField
defaultText: condition.pattern
width: Math.min(implicitWidth, parent.width)
placeholderText: ({
"content.body": qsTr("text..."),
"content.msgtype": qsTr("e.g. m.image"),
"room_id": qsTr("!room:example.org"),
"sender": qsTr("@user:example.org"),
"state_key": qsTr("@user:example.org"),
"type": qsTr("e.g. m.room.message"),
}[keyCombo.editText] || qsTr("value"))
}
}

View File

@ -0,0 +1,48 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import ".."
import "../../../Base"
CustomFlow {
readonly property var matrixObject: ({
kind: "room_member_count",
is: operatorCombo.operators[operatorCombo.currentIndex]
.replace("==", "") + countSpin.value,
})
CustomLabel {
text: qsTr("Room has")
verticalAlignment: CustomLabel.AlignVCenter
height: operatorCombo.height
}
HComboBox {
readonly property var operators: ["==", ">=", "<=", ">", "<"]
id: operatorCombo
width: Math.min(implicitWidth, parent.width)
currentIndex: operators.indexOf(/[=<>]+/.exec(condition.is + "==")[0])
model: [
qsTr("exactly"),
qsTr("at least"),
qsTr("at most"),
qsTr("more than"),
qsTr("less than"),
]
}
HSpinBox {
id: countSpin
width: Math.min(implicitWidth, parent.width)
defaultValue: parseInt(condition.is.replace(/[=<>]/, ""), 10)
}
CustomLabel {
text: qsTr("members")
verticalAlignment: CustomLabel.AlignVCenter
height: operatorCombo.height
}
}

View File

@ -0,0 +1,35 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import ".."
import "../../../Base"
CustomFlow {
readonly property var matrixObject: ({
kind: "sender_notification_permission",
key: keyCombo.editText,
})
CustomLabel {
text: qsTr("Sender has permission to send")
verticalAlignment: CustomLabel.AlignVCenter
height: keyCombo.height
}
HComboBox {
id: keyCombo
width: Math.min(implicitWidth, parent.width)
editable: true
editText: condition.key
currentIndex: model.indexOf(condition.key)
model: [...new Set(["room", condition.key])].sort()
}
CustomLabel {
text: qsTr("notifications")
verticalAlignment: CustomLabel.AlignVCenter
height: keyCombo.height
}
}

View File

@ -0,0 +1,36 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import ".."
import "../../../Base"
AutoDirectionLayout {
readonly property var matrixObject: {
try {
JSON.parse(jsonField.text)
} catch (e) {
// TODO
return condition.condition
}
}
rowSpacing: theme.spacing / 2
columnSpacing: rowSpacing
CustomLabel {
text: qsTr("Custom JSON:")
verticalAlignment: CustomLabel.AlignVCenter
Layout.fillWidth: false
Layout.fillHeight: true
}
HTextField {
// TODO: validate the JSON
id: jsonField
font.family: theme.fontFamily.mono
defaultText: JSON.stringify(condition.condition)
Layout.fillWidth: true
}
}

View File

@ -0,0 +1,243 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import ".."
import "../.."
import "../../Base"
import "../../Base/Buttons"
HFlickableColumnPopup {
id: root
property string userId
// A rule item from ModelStore.get(userId, "pushrules")
property var rule
readonly property bool generalChecked:
overrideRadio.checked || underrideRadio.checked
readonly property string checkedKind:
overrideRadio.checked ? "override" :
contentRadio.checked ? "content" :
roomRadio.checked ? "room" :
senderRadio.checked ? "sender" :
"underride"
function save() {
const details = swipeView.currentItem
const isBefore = positionCombo.currentIndex === 0
const position =
positionCombo.visible && ! positionCombo.isCurrent ?
positionCombo.model[positionCombo.currentIndex].rule_id :
undefined
const args = [
checkedKind,
details.idField.text,
rule.kind,
rule.rule_id,
isBefore && position ? position : undefined,
! isBefore && position ? position : undefined,
enableCheck.checked,
generalChecked ? details.matrixConditions : undefined,
contentRadio.checked ? details.idField.text : undefined,
]
py.callClientCoro(userId, "edit_pushrule", args, root.close)
}
page.implicitWidth: Math.min(maximumPreferredWidth, 550 * theme.uiScale)
page.footer: AutoDirectionLayout {
ApplyButton {
text: qsTr("Save changes")
enabled: true // TODO
onClicked: root.save()
}
CancelButton {
text: qsTr("Cancel changes")
onClicked: root.close()
}
NegativeButton {
icon.name: "pushrule-remove"
text: qsTr("Remove rule")
enabled: ! root.rule.default
}
}
CustomLabel {
visible: root.rule.default
text: qsTr("Some settings cannot be changed for default server rules")
color: theme.colors.warningText
}
HColumnLayout {
enabled: ! root.rule.default
spacing: theme.spacing / 2
CustomLabel {
text: qsTr("Rule type:")
}
HRadioButton {
id: overrideRadio
text: "High priority general rule"
subtitle.text: qsTr(
"Control notifications for messages matching certain " +
"conditions"
)
defaultChecked: root.rule.kind === "override"
Layout.fillWidth: true
}
HRadioButton {
id: contentRadio
text: "Message content rule"
subtitle.text: qsTr(
"Control notifications for text messages containing a " +
"certain word"
)
defaultChecked: root.rule.kind === "content"
Layout.fillWidth: true
}
HRadioButton {
id: roomRadio
text: "Room rule"
subtitle.text: qsTr(
"Control notifications for all messages received in a " +
"certain room"
)
defaultChecked: root.rule.kind === "room"
Layout.fillWidth: true
}
HRadioButton {
id: senderRadio
text: "Sender rule"
subtitle.text: qsTr(
"Control notifications for all messages sent by a " +
"certain user"
)
defaultChecked: root.rule.kind === "sender"
Layout.fillWidth: true
}
HRadioButton {
id: underrideRadio
text: "Low priority general rule"
subtitle.text: qsTr(
"A general rule tested only after every other rule types"
)
defaultChecked: root.rule.kind === "underride"
Layout.fillWidth: true
}
}
SwipeView {
id: swipeView
enabled: ! root.rule.default
clip: true
interactive: false
currentIndex:
overrideRadio.checked ? 0 :
contentRadio.checked ? 1 :
roomRadio.checked ? 2 :
senderRadio.checked ? 3 :
4
Layout.fillWidth: true
Behavior on implicitHeight { HNumberAnimation {} }
GeneralRule { enabled: SwipeView.isCurrentItem }
ContentRule { enabled: SwipeView.isCurrentItem }
RoomRule { enabled: SwipeView.isCurrentItem }
SenderRule { enabled: SwipeView.isCurrentItem }
GeneralRule { enabled: SwipeView.isCurrentItem }
}
HLabeledItem {
visible: ! rule.default && positionCombo.model.length > 1
label.text: qsTr("Position:")
Layout.fillWidth: true
HComboBox {
id: positionCombo
property int currentPosition: 0
readonly property string name:
! model.length ? "" : utils.stripHtmlTags(
utils.formatPushRuleName(root.userId, model[currentIndex])
)
readonly property bool isCurrent:
model.length &&
currentIndex === currentPosition &&
root.rule.kind === root.checkedKind
width: parent.width
currentIndex: currentPosition
displayText:
! model.length ? "" :
isCurrent ? qsTr("Current") :
currentIndex === 0 ? qsTr('Before "%1"').arg(name) :
qsTr('After "%1"').arg(name)
model: {
currentPosition = 0
const choices = []
const rules = ModelStore.get(userId, "pushrules")
for (let i = 0; i < rules.count; i++) {
const item = rules.get(i)
const isCurrent =
item.kind === root.checkedKind &&
item.rule_id === root.rule.rule_id
if (isCurrent && choices.length)
currentPosition = choices.length - 1
if (item.kind === root.checkedKind && ! item.default) {
if (! choices.length) choices.push(item)
if (! isCurrent) choices.push(item)
}
}
return choices
}
delegate: HMenuItem {
readonly property string name:
utils.formatPushRuleName(root.userId, modelData)
label.textFormat: HLabel.StyledText
text:
model.index === positionCombo.currentPosition &&
root.rule.kind === root.checkedKind ?
qsTr("Current") :
model.index === 0 ?
qsTr('Before "%1"').arg(name) :
qsTr('After "%1"').arg(name)
onTriggered: positionCombo.currentIndex = model.index
}
}
}
HCheckBox {
id: enableCheck
text: qsTr("Enable this rule")
defaultChecked: root.rule.enabled
Layout.fillWidth: true
}
}

View File

@ -0,0 +1,21 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
import "../../Base/Buttons"
HColumnLayout {
readonly property alias idField: idField
HLabeledItem {
label.text: qsTr("Room ID:")
Layout.fillWidth: true
HTextField {
id: idField
width: parent.width
defaultText: rule.kind === "room" ? rule.rule_id : ""
placeholderText: qsTr("!room:example.org")
maximumLength: 255
}
}
}

View File

@ -0,0 +1,21 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
import "../../Base/Buttons"
HColumnLayout {
readonly property alias idField: idField
HLabeledItem {
label.text: qsTr("User ID:")
Layout.fillWidth: true
HTextField {
id: idField
width: parent.width
defaultText: rule.kind === "sender" ? rule.rule_id : ""
placeholderText: qsTr("@alice:example.org")
maximumLength: 255
}
}
}

View File

@ -535,6 +535,7 @@ QtObject {
return {word, start, end: seen} return {word, start, end: seen}
} }
function getClassPathRegex(obj) { function getClassPathRegex(obj) {
const regexParts = [] const regexParts = []
let parent = obj let parent = obj
@ -557,4 +558,79 @@ QtObject {
return new RegExp("^" + regexParts.reverse().join("") + "$") return new RegExp("^" + regexParts.reverse().join("") + "$")
} }
function formatPushRuleName(userId, rule) {
// rule: item from ModelStore.get(<userId>, "pushrules")
const roomColor = theme.colors.accentText
const room = ModelStore.get(userId, "rooms").find(rule.rule_id)
return (
rule.rule_id === ".m.rule.master" ?
qsTr("Any message") :
rule.rule_id === ".m.rule.suppress_notices" ?
qsTr("Messages sent by bots") :
rule.rule_id === ".m.rule.invite_for_me" ?
qsTr("Received room invites") :
rule.rule_id === ".m.rule.member_event" ?
qsTr("Membership, name & avatar changes") :
rule.rule_id === ".m.rule.contains_display_name" ?
qsTr("Messages containing my display name") :
rule.rule_id === ".m.rule.tombstone" ?
qsTr("Room migration alerts") :
rule.rule_id === ".m.rule.reaction" ?
qsTr("Emoji reactions") :
rule.rule_id === ".m.rule.roomnotif" ?
qsTr("Messages containing %1").arg(
htmlColorize("@room", roomColor),
) :
rule.rule_id === ".m.rule.contains_user_name" ?
qsTr("Contains %1").arg(coloredNameHtml(
"", userId, userId.split(":")[0].substring(1),
)):
rule.rule_id === ".m.rule.call" ?
qsTr("Incoming audio calls") :
rule.rule_id === ".m.rule.encrypted_room_one_to_one" ?
qsTr("Encrypted 1-to-1 messages") :
rule.rule_id === ".m.rule.room_one_to_one" ?
qsTr("Unencrypted 1-to-1 messages") :
rule.rule_id === ".m.rule.message" ?
qsTr("Unencrypted group messages") :
rule.rule_id === ".m.rule.encrypted" ?
qsTr("Encrypted group messages") :
rule.rule_id === ".im.vector.jitsi" ?
qsTr("Incoming Jitsi calls") :
rule.kind === "content" ?
qsTr('Contains "%1"').arg(rule.pattern) :
rule.kind === "sender" ?
coloredNameHtml("", rule.rule_id) :
room && room.display_name && rule.kind !== "room" ?
qsTr("Messages in room %1").arg(
htmlColorize(escapeHtml(room.display_name), roomColor)
) :
room && room.display_name ?
escapeHtml(room.display_name) :
escapeHtml(rule.rule_id)
)
}
} }

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 7.33l2.829-2.83 9.175 9.339 9.167-9.339 2.829 2.83-11.996 12.17z"/></svg>

After

Width:  |  Height:  |  Size: 168 B

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="m24 10h-10v-10h-4v10h-10v4h10v10h4v-10h10z"/>
</svg>

After

Width:  |  Height:  |  Size: 150 B

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="m23 20.168-8.185-8.187 8.185-8.174-2.832-2.807-8.182 8.179-8.176-8.179-2.81 2.81 8.186 8.196-8.186 8.184 2.81 2.81 8.203-8.192 8.18 8.192z"/>
</svg>

After

Width:  |  Height:  |  Size: 246 B

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="m9 19c0 .552-.448 1-1 1s-1-.448-1-1v-10c0-.552.448-1 1-1s1 .448 1 1zm4 0c0 .552-.448 1-1 1s-1-.448-1-1v-10c0-.552.448-1 1-1s1 .448 1 1zm4 0c0 .552-.448 1-1 1s-1-.448-1-1v-10c0-.552.448-1 1-1s1 .448 1 1zm5-17v2h-20v-2h5.711c.9 0 1.631-1.099 1.631-2h5.315c0 .901.73 2 1.631 2zm-3 4v16h-14v-16h-2v18h18v-18z"/>
</svg>

After

Width:  |  Height:  |  Size: 412 B