Initial work to add push rule support

This commit adds the ability to set the master push rule and set push rules for individual rooms as per the matrix spec. See https://spec.matrix.org/v1.3/client-server-api/#push-rules.

The master push rule is just on/off and uses the existing notification setting in general setting to enable/disable the server default master push rule .m.rule.master.

For each room there is now a page in the room setting that allows the following to be set:
- Default
- All messages
- @mentions and keywords
- off

New room or override rules are added/removed to achieve this.

There is also functionality to check the master/room notification state whenever the setting menu is entered. This allows the status to be updated if changed in another client or get the initial state for a room as it isn't stored.

Note - There is currently no menu items in the room list for setting the room push rule settings. This will be added in a later commit, the aim is to focus on making sure the technical implementation is good for now.
This commit is contained in:
James Graham
2022-09-09 16:41:03 +00:00
parent c2fc4e44a7
commit 4bba505da6
11 changed files with 353 additions and 5 deletions

View File

@@ -3,12 +3,12 @@
import QtQuick 2.15
import org.kde.kirigami 2.18 as Kirigami
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
Kirigami.CategorizedSettings {
id: root
property var room
objectName: "settingsPage"
actions: [
Kirigami.SettingAction {
@@ -30,6 +30,16 @@ Kirigami.CategorizedSettings {
room: root.room
}
}
},
Kirigami.SettingAction {
text: i18n("Notifications")
icon.name: "notifications"
page: Qt.resolvedUrl("PushNotification.qml")
initialProperties: {
return {
room: root.room
}
}
}
]
}

View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
property var room
title: i18nc('@title:window', 'Notifications')
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
QQC2.RadioButton {
text: i18n("Default")
Kirigami.FormData.label: i18n("Room notifications setting:")
checked: room.pushNotificationState === PushNotificationState.Default
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.Default
}
}
QQC2.RadioButton {
text: i18n("All messages")
checked: room.pushNotificationState === PushNotificationState.All
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.All
}
}
QQC2.RadioButton {
text: i18n("@mentions and keywords")
checked: room.pushNotificationState === PushNotificationState.MentionKeyword
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.MentionKeyword
}
}
QQC2.RadioButton {
text: i18n("Off")
checked: room.pushNotificationState === PushNotificationState.Mute
enabled: room.pushNotificationState != PushNotificationState.Unknown
onToggled: {
room.pushNotificationState = PushNotificationState.Mute
}
}
}
}
}

View File

@@ -46,6 +46,7 @@ Kirigami.ScrollablePage {
onToggled: {
Config.showNotifications = checked
Config.save()
NotificationsManager.globalNotificationsEnabled = checked
}
}
QQC2.CheckBox {

View File

@@ -3,7 +3,6 @@
import QtQuick 2.15
import org.kde.kirigami 2.18 as Kirigami
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
Kirigami.CategorizedSettings {

View File

@@ -15,6 +15,7 @@
<file>imports/NeoChat/Page/WelcomePage.qml</file>
<file>imports/NeoChat/RoomSettings/General.qml</file>
<file>imports/NeoChat/RoomSettings/Security.qml</file>
<file>imports/NeoChat/RoomSettings/PushNotification.qml</file>
<file>imports/NeoChat/RoomSettings/Categories.qml</file>
<file>imports/NeoChat/Component/qmldir</file>
<file>imports/NeoChat/Component/FullScreenImage.qml</file>

View File

@@ -139,6 +139,7 @@ if(ANDROID)
"org.kde.neochat"
"preferences-system-users"
"preferences-desktop-theme-global"
"notifications"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets)

View File

@@ -184,6 +184,7 @@ int main(int argc, char *argv[])
#endif
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "NotificationsManager", &NotificationsManager::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "RoomManager", &RoomManager::instance());
@@ -210,6 +211,7 @@ int main(int argc, char *argv[])
qmlRegisterType<SortFilterSpaceListModel>("org.kde.neochat", 1, 0, "SortFilterSpaceListModel");
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM");
qmlRegisterUncreatableType<UserType>("org.kde.neochat", 1, 0, "UserType", "ENUM");

View File

@@ -23,6 +23,7 @@
#include <csapi/account-data.h>
#include <csapi/content-repo.h>
#include <csapi/leaving.h>
#include <csapi/pushrules.h>
#include <csapi/redaction.h>
#include <csapi/room_state.h>
#include <csapi/rooms.h>
@@ -39,6 +40,7 @@
#include <qt_connection_util.h>
#include <user.h>
#include "controller.h"
#include "neochatconfig.h"
#include "notificationsmanager.h"
#include "stickerevent.h"
@@ -49,6 +51,7 @@
NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinState)
: Room(connection, std::move(roomId), joinState)
{
connect(connection, &Connection::accountDataChanged, this, &NeoChatRoom::updatePushNotificationState);
connect(this, &NeoChatRoom::notificationCountChanged, this, &NeoChatRoom::countChanged);
connect(this, &NeoChatRoom::highlightCountChanged, this, &NeoChatRoom::countChanged);
connect(this, &Room::fileTransferCompleted, this, [this] {
@@ -886,3 +889,196 @@ bool NeoChatRoom::isSpace()
return false;
#endif
}
void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
{
// The caller should never try to set the state to unknown.
// It exists only as a default state to diable the settings options until the actual state is retrieved from the server.
if (state == PushNotificationState::Unknown) {
Q_ASSERT(false);
return;
}
/**
* This stops updatePushNotificationState from temporarily changing
* m_pushNotificationStateUpdating to default after the exisitng rules are deleted but
* before a new rule is added.
* The value is set to false after the rule enable job is successful.
*/
m_pushNotificationStateUpdating = true;
/**
* First remove any exisiting room rules of the wrong type.
* Note to prevent race conditions any rule that is going ot be overridden later is not removed.
* If the default push notification state is chosen any exisiting rule needs to be removed.
*/
QJsonObject accountData = connection()->accountDataJson("m.push_rules");
// For default and mute check for a room rule and remove if found.
if (state == PushNotificationState::Default || state == PushNotificationState::Mute) {
QJsonArray roomRuleArray = accountData["global"].toObject()["room"].toArray();
for (const auto &i : roomRuleArray) {
QJsonObject roomRule = i.toObject();
if (roomRule["rule_id"] == id()) {
Controller::instance().activeConnection()->callApi<DeletePushRuleJob>("global", "room", id());
}
}
}
// For default, all and @mentions and keywords check for an override rule and remove if found.
if (state == PushNotificationState::Default || state == PushNotificationState::All || state == PushNotificationState::MentionKeyword) {
QJsonArray overrideRuleArray = accountData["global"].toObject()["override"].toArray();
for (const auto &i : overrideRuleArray) {
QJsonObject overrideRule = i.toObject();
if (overrideRule["rule_id"] == id()) {
Controller::instance().activeConnection()->callApi<DeletePushRuleJob>("global", "override", id());
}
}
}
if (state == PushNotificationState::Mute) {
/**
* To mute a room an override rule with "don't notify is set".
*
* Setup the rule action to "don't notify" to stop all room notifications
* see https://spec.matrix.org/v1.3/client-server-api/#actions
*
* "actions": [
* "don't_notify"
* ]
*/
const QVector<QVariant> actions = {"dont_notify"};
/**
* Setup the push condition to get all events for the current room
* see https://spec.matrix.org/v1.3/client-server-api/#conditions-1
*
* "conditions": [
* {
* "key": "type",
* "kind": "event_match",
* "pattern": "room_id"
* }
* ]
*/
PushCondition pushCondition;
pushCondition.kind = "event_match";
pushCondition.key = "room_id";
pushCondition.pattern = id();
const QVector<PushCondition> conditions = {pushCondition};
// Add new override rule and make sure it's enabled
auto job = Controller::instance().activeConnection()->callApi<SetPushRuleJob>("global", "override", id(), actions, "", "", conditions, "");
connect(job, &BaseJob::success, this, [this]() {
auto enableJob = Controller::instance().activeConnection()->callApi<SetPushRuleEnabledJob>("global", "override", id(), true);
connect(enableJob, &BaseJob::success, this, [this]() {
m_pushNotificationStateUpdating = false;
});
});
} else if (state == PushNotificationState::MentionKeyword) {
/**
* To only get notifcations for @ mentions and keywords a room rule with "don't_notify" is set.
*
* Note - This works becuase a default override rule which catches all user mentions will
* take precedent and notify. See https://spec.matrix.org/v1.3/client-server-api/#default-override-rules. Any keywords will also have a similar override
* rule.
*
* Setup the rule action to "don't notify" to stop all room event notifications
* see https://spec.matrix.org/v1.3/client-server-api/#actions
*
* "actions": [
* "don't_notify"
* ]
*/
const QVector<QVariant> actions = {"dont_notify"};
// No conditions for a room rule
const QVector<PushCondition> conditions;
auto setJob = Controller::instance().activeConnection()->callApi<SetPushRuleJob>("global", "room", id(), actions, "", "", conditions, "");
connect(setJob, &BaseJob::success, this, [this]() {
auto enableJob = Controller::instance().activeConnection()->callApi<SetPushRuleEnabledJob>("global", "room", id(), true);
connect(enableJob, &BaseJob::success, this, [this]() {
m_pushNotificationStateUpdating = false;
});
});
} else if (state == PushNotificationState::All) {
/**
* To send a notification for all room messages a room rule with "notify" is set.
*
* Setup the rule action to "notify" so all room events give notifications.
* Tweeks is also set to follow default sound settings
* see https://spec.matrix.org/v1.3/client-server-api/#actions
*
* "actions": [
* "notify",
* {
* "set_tweek": "sound",
* "value": "default",
* }
* ]
*/
QJsonObject tweaks;
tweaks.insert("set_tweak", "sound");
tweaks.insert("value", "default");
const QVector<QVariant> actions = {"notify", tweaks};
// No conditions for a room rule
const QVector<PushCondition> conditions;
// Add new room rule and make sure enabled
auto setJob = Controller::instance().activeConnection()->callApi<SetPushRuleJob>("global", "room", id(), actions, "", "", conditions, "");
connect(setJob, &BaseJob::success, this, [this]() {
auto enableJob = Controller::instance().activeConnection()->callApi<SetPushRuleEnabledJob>("global", "room", id(), true);
connect(enableJob, &BaseJob::success, this, [this]() {
m_pushNotificationStateUpdating = false;
});
});
}
m_currentPushNotificationState = state;
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
}
void NeoChatRoom::updatePushNotificationState(QString type)
{
if (type != "m.push_rules" || m_pushNotificationStateUpdating) {
return;
}
QJsonObject accountData = connection()->accountDataJson("m.push_rules");
// First look for a room rule with the room id
QJsonArray roomRuleArray = accountData["global"].toObject()["room"].toArray();
for (const auto &i : roomRuleArray) {
QJsonObject roomRule = i.toObject();
if (roomRule["rule_id"] == id()) {
QString notifyAction = roomRule["actions"].toArray()[0].toString();
if (notifyAction == "notify") {
m_currentPushNotificationState = PushNotificationState::All;
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
return;
} else if (notifyAction == "dont_notify") {
m_currentPushNotificationState = PushNotificationState::MentionKeyword;
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
return;
}
}
}
// Check for an override rule with the room id
QJsonArray overrideRuleArray = accountData["global"].toObject()["override"].toArray();
for (const auto &i : overrideRuleArray) {
QJsonObject overrideRule = i.toObject();
if (overrideRule["rule_id"] == id()) {
QString notifyAction = overrideRule["actions"].toArray()[0].toString();
if (notifyAction == "dont_notify") {
m_currentPushNotificationState = PushNotificationState::Mute;
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
return;
}
}
}
// If neither a room or override rule exist for the room then the setting must be default
m_currentPushNotificationState = PushNotificationState::Default;
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
}

View File

@@ -23,6 +23,21 @@
using namespace Quotient;
class PushNotificationState : public QObject
{
Q_OBJECT
public:
enum State {
Unknown,
Default,
Mute,
MentionKeyword,
All,
};
Q_ENUM(State);
};
class NeoChatRoom : public Room
{
Q_OBJECT
@@ -36,6 +51,8 @@ class NeoChatRoom : public Room
Q_PROPERTY(bool isInvite READ isInvite NOTIFY isInviteChanged)
Q_PROPERTY(QString joinRule READ joinRule CONSTANT)
Q_PROPERTY(QString htmlSafeDisplayName READ htmlSafeDisplayName NOTIFY displayNameChanged)
Q_PROPERTY(PushNotificationState::State pushNotificationState MEMBER m_currentPushNotificationState WRITE setPushNotificationState NOTIFY
pushNotificationStateChanged)
public:
explicit NeoChatRoom(Connection *connection, QString roomId, JoinState joinState = {});
@@ -131,6 +148,8 @@ public:
Q_INVOKABLE QString htmlSafeDisplayName() const;
Q_INVOKABLE void clearInvitationNotification();
Q_INVOKABLE void setPushNotificationState(PushNotificationState::State state);
#ifndef QUOTIENT_07
Q_INVOKABLE QString htmlSafeMemberName(const QString &userId) const
{
@@ -145,6 +164,9 @@ private:
bool m_hasFileUploading = false;
int m_fileUploadingProgress = 0;
PushNotificationState::State m_currentPushNotificationState = PushNotificationState::State::Unknown;
bool m_pushNotificationStateUpdating = false;
void checkForHighlights(const Quotient::TimelineItem &ti);
void onAddNewTimelineEvents(timeline_iter_t from) override;
@@ -157,6 +179,7 @@ private:
private Q_SLOTS:
void countChanged();
void updatePushNotificationState(QString type);
Q_SIGNALS:
void cachedInputChanged();
@@ -168,6 +191,7 @@ Q_SIGNALS:
void lastActiveTimeChanged();
void isInviteChanged();
void displayNameChanged();
void pushNotificationStateChanged(PushNotificationState::State state);
public Q_SLOTS:
void uploadFile(const QUrl &url, const QString &body = QString());

View File

@@ -16,6 +16,9 @@
#endif
#include <KNotificationReplyAction>
#include "csapi/pushrules.h"
#include "jobs/basejob.h"
#include "controller.h"
#include "neochatconfig.h"
#include "roommanager.h"
@@ -30,6 +33,10 @@ NotificationsManager &NotificationsManager::instance()
NotificationsManager::NotificationsManager(QObject *parent)
: QObject(parent)
{
// Can't connect the signal up until the active connection has been established by the controller
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
connect(Controller::instance().activeConnection(), &Connection::accountDataChanged, this, &NotificationsManager::updateGlobalNotificationsEnabled);
});
}
void NotificationsManager::postNotification(NeoChatRoom *room,
@@ -73,7 +80,6 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id());
notification->sendEvent();
m_notifications.insert(room->id(), notification);
@@ -98,11 +104,11 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QStri
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
});
notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")});
connect(notification, &KNotification::action1Activated, this, [this, room, notification]() {
connect(notification, &KNotification::action1Activated, this, [room, notification]() {
room->acceptInvitation();
notification->close();
});
connect(notification, &KNotification::action2Activated, this, [this, room, notification]() {
connect(notification, &KNotification::action2Activated, this, [room, notification]() {
RoomManager::instance().leaveRoom(room);
notification->close();
});
@@ -122,3 +128,42 @@ void NotificationsManager::clearInvitationNotification(const QString &roomId)
m_invitations[roomId]->close();
}
}
/**
* The master push rule sets all notifications to off when enabled
* see https://spec.matrix.org/v1.3/client-server-api/#default-override-rules
* therefore to enable push rules the master rule needs to be disabled and vice versa
*/
void NotificationsManager::setGlobalNotificationsEnabled(bool enabled)
{
using namespace Quotient;
auto job = Controller::instance().activeConnection()->callApi<IsPushRuleEnabledJob>("global", "override", ".m.rule.master");
connect(job, &BaseJob::success, this, [this, job, enabled]() {
if (job->enabled() == enabled) {
Controller::instance().activeConnection()->callApi<SetPushRuleEnabledJob>("global", "override", ".m.rule.master", !enabled);
m_globalNotificationsEnabled = enabled;
Q_EMIT globalNotificationsEnabledChanged(m_globalNotificationsEnabled);
}
});
}
void NotificationsManager::updateGlobalNotificationsEnabled(QString type)
{
if (type != "m.push_rules") {
return;
}
QJsonObject accountData = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
QJsonArray overrideRuleArray = accountData.value("global").toObject().value("override").toArray();
for (const auto &i : overrideRuleArray) {
QJsonObject overrideRule = i.toObject();
if (overrideRule.value("rule_id") == ".m.rule.master") {
bool ruleEnabled = overrideRule.value("enabled").toBool();
m_globalNotificationsEnabled = !ruleEnabled;
NeoChatConfig::self()->setShowNotifications(m_globalNotificationsEnabled);
Q_EMIT globalNotificationsEnabledChanged(m_globalNotificationsEnabled);
}
}
}

View File

@@ -10,11 +10,13 @@
#include <KNotification>
#include "neochatconfig.h"
#include "neochatroom.h"
class NotificationsManager : public QObject
{
Q_OBJECT
Q_PROPERTY(bool globalNotificationsEnabled MEMBER m_globalNotificationsEnabled WRITE setGlobalNotificationsEnabled NOTIFY globalNotificationsEnabledChanged)
public:
static NotificationsManager &instance();
@@ -25,9 +27,19 @@ public:
void clearInvitationNotification(const QString &roomId);
Q_INVOKABLE void setGlobalNotificationsEnabled(bool enabled);
private:
NotificationsManager(QObject *parent = nullptr);
QMultiMap<QString, KNotification *> m_notifications;
QHash<QString, QPointer<KNotification>> m_invitations;
bool m_globalNotificationsEnabled;
private Q_SLOTS:
void updateGlobalNotificationsEnabled(QString type);
Q_SIGNALS:
void globalNotificationsEnabledChanged(bool newState);
};