Add popup to edit push rules
This commit is contained in:
parent
71cd509a9d
commit
765ce46aeb
16
docs/TODO.md
16
docs/TODO.md
|
@ -1,5 +1,21 @@
|
|||
# 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 error handling
|
||||
- Change docs linking to dev branch back to master
|
||||
|
|
|
@ -41,8 +41,8 @@ from .errors import (
|
|||
from .html_markdown import HTML_PROCESSOR as HTML
|
||||
from .media_cache import Media, Thumbnail
|
||||
from .models.items import (
|
||||
ZERO_DATE, Account, Event, Member, PushRule, Room,
|
||||
Transfer, TransferStatus, TypeSpecifier,
|
||||
ZERO_DATE, Account, Event, Member, PushRule, Room, Transfer,
|
||||
TransferStatus, TypeSpecifier,
|
||||
)
|
||||
from .models.model_store import ModelStore
|
||||
from .nio_callbacks import NioCallbacks
|
||||
|
@ -54,6 +54,8 @@ from .pyotherside_events import (
|
|||
if TYPE_CHECKING:
|
||||
from .backend import Backend
|
||||
|
||||
PushAction = Union[Dict[str, Any], nio.PushAction]
|
||||
PushCondition = Union[Dict[str, Any], nio.PushCondition]
|
||||
CryptDict = Dict[str, Any]
|
||||
PathCallable = Union[
|
||||
str, Path, Callable[[], Coroutine[None, None, Union[str, Path]]],
|
||||
|
@ -1780,6 +1782,79 @@ class MatrixClient(nio.AsyncClient):
|
|||
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(
|
||||
self,
|
||||
kind: Union[nio.PushRuleKind, str],
|
||||
|
@ -1790,6 +1865,7 @@ class MatrixClient(nio.AsyncClient):
|
|||
sound: Optional[str] = None,
|
||||
urgency_hint: Optional[bool] = None,
|
||||
) -> None:
|
||||
"""Set an existing pushrule's actions. Works for builtin rules."""
|
||||
|
||||
kind = nio.PushRuleKind[kind] if isinstance(kind, str) else kind
|
||||
|
||||
|
|
|
@ -94,7 +94,9 @@ class PushRule(ModelItem):
|
|||
order: int = field()
|
||||
default: bool = field()
|
||||
enabled: bool = True
|
||||
conditions: List[Dict[str, Any]] = field(default_factory=list)
|
||||
pattern: str = ""
|
||||
actions: List[Dict[str, Any]] = field(default_factory=list)
|
||||
notify: bool = False
|
||||
highlight: bool = False
|
||||
bubble: bool = False
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import asyncio
|
||||
import json
|
||||
import logging as log
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
|
@ -786,7 +786,8 @@ class NioCallbacks:
|
|||
model = self.models[self.user_id, "pushrules"]
|
||||
|
||||
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.
|
||||
|
@ -831,7 +832,9 @@ class NioCallbacks:
|
|||
order = order,
|
||||
default = rule.default,
|
||||
enabled = rule.enabled,
|
||||
conditions = [c.as_value for c in rule.conditions],
|
||||
pattern = rule.pattern,
|
||||
actions = [a.as_value for a in rule.actions],
|
||||
notify = notify,
|
||||
highlight = high,
|
||||
bubble = bubble,
|
||||
|
|
71
src/gui/Base/HSpinBox.qml
Normal file
71
src/gui/Base/HSpinBox.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.12
|
||||
|
@ -5,19 +6,15 @@ import QtQuick.Layouts 1.12
|
|||
import "../.."
|
||||
import "../../Base"
|
||||
import "../../Base/HTile"
|
||||
import "../../Base/Buttons"
|
||||
import "../../MainPane"
|
||||
import "../../Popups"
|
||||
|
||||
HTile {
|
||||
id: root
|
||||
|
||||
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
|
||||
hoverEnabled: false
|
||||
leftPadding: theme.spacing / 4
|
||||
|
@ -30,71 +27,8 @@ HTile {
|
|||
opacity: model.enabled ? 1 : theme.disabledElementsOpacity
|
||||
elide: Text.ElideNone
|
||||
wrapMode: HLabel.Wrap
|
||||
|
||||
textFormat:
|
||||
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
|
||||
textFormat: HLabel.StyledText
|
||||
text: utils.formatPushRuleName(page.userId, model)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: theme.spacing
|
||||
|
@ -161,6 +95,10 @@ HTile {
|
|||
|
||||
NotificationRuleButton {
|
||||
icon.name: "pushrule-edit"
|
||||
onClicked: window.makePopup(
|
||||
"Popups/PushRuleSettingsPopup/PushRuleSettingsPopup.qml",
|
||||
{userId: page.userId, rule: model},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,11 +73,11 @@ HListView {
|
|||
padding: theme.spacing
|
||||
font.pixelSize: theme.fontSize.big
|
||||
text:
|
||||
section === "override" ? qsTr("High-priority general rules") :
|
||||
section === "content" ? qsTr("Message text rules") :
|
||||
section === "override" ? qsTr("High priority general rules") :
|
||||
section === "content" ? qsTr("Message content rules") :
|
||||
section === "room" ? qsTr("Room rules") :
|
||||
section === "sender" ? qsTr("Sender rules") :
|
||||
qsTr("General rules")
|
||||
qsTr("Low priority general rules")
|
||||
}
|
||||
|
||||
delegate: NotificationRuleDelegate {
|
||||
|
|
20
src/gui/Popups/PushRuleSettingsPopup/ContentRule.qml
Normal file
20
src/gui/Popups/PushRuleSettingsPopup/ContentRule.qml
Normal 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 : ""
|
||||
}
|
||||
}
|
||||
}
|
14
src/gui/Popups/PushRuleSettingsPopup/CustomLabel.qml
Normal file
14
src/gui/Popups/PushRuleSettingsPopup/CustomLabel.qml
Normal 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 {} }
|
||||
}
|
119
src/gui/Popups/PushRuleSettingsPopup/GeneralRule.qml
Normal file
119
src/gui/Popups/PushRuleSettingsPopup/GeneralRule.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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"))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
243
src/gui/Popups/PushRuleSettingsPopup/PushRuleSettingsPopup.qml
Normal file
243
src/gui/Popups/PushRuleSettingsPopup/PushRuleSettingsPopup.qml
Normal 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
|
||||
}
|
||||
}
|
21
src/gui/Popups/PushRuleSettingsPopup/RoomRule.qml
Normal file
21
src/gui/Popups/PushRuleSettingsPopup/RoomRule.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
21
src/gui/Popups/PushRuleSettingsPopup/SenderRule.qml
Normal file
21
src/gui/Popups/PushRuleSettingsPopup/SenderRule.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -535,6 +535,7 @@ QtObject {
|
|||
return {word, start, end: seen}
|
||||
}
|
||||
|
||||
|
||||
function getClassPathRegex(obj) {
|
||||
const regexParts = []
|
||||
let parent = obj
|
||||
|
@ -557,4 +558,79 @@ QtObject {
|
|||
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
1
src/icons/thin/combo-box-menu.svg
Normal file
1
src/icons/thin/combo-box-menu.svg
Normal 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 |
3
src/icons/thin/pushrule-condition-add.svg
Normal file
3
src/icons/thin/pushrule-condition-add.svg
Normal 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 |
3
src/icons/thin/pushrule-condition-remove.svg
Normal file
3
src/icons/thin/pushrule-condition-remove.svg
Normal 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 |
3
src/icons/thin/pushrule-remove.svg
Normal file
3
src/icons/thin/pushrule-remove.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user