diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9a36e3fe5..c7823c9fe 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,7 +24,7 @@ add_library(neochat STATIC models/userfiltermodel.cpp models/publicroomlistmodel.cpp models/userdirectorylistmodel.cpp - models/keywordnotificationrulemodel.cpp + models/pushrulemodel.cpp models/emoticonfiltermodel.cpp notificationsmanager.cpp models/sortfilterroomlistmodel.cpp diff --git a/src/controller.cpp b/src/controller.cpp index d4ebcfdcd..59e5c857f 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "controller.h" +#include "models/pushrulemodel.h" #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include @@ -126,6 +127,10 @@ Controller::Controller(QObject *parent) oldAccountCount = Accounts.size(); }); #endif + + QTimer::singleShot(0, this, [this] { + m_pushRuleModel = new PushRuleModel; + }); } Controller &Controller::instance() @@ -507,6 +512,11 @@ void Controller::setActiveConnection(Connection *connection) Q_EMIT activeAccountLabelChanged(); } +PushRuleModel *Controller::pushRuleModel() const +{ + return m_pushRuleModel; +} + void Controller::saveWindowGeometry() { WindowController::instance().saveGeometry(); diff --git a/src/controller.h b/src/controller.h index efdb46ed7..2c074e8d4 100644 --- a/src/controller.h +++ b/src/controller.h @@ -3,6 +3,7 @@ #pragma once +#include "models/pushrulemodel.h" #include #include @@ -50,6 +51,11 @@ class Controller : public QObject */ Q_PROPERTY(Quotient::Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged) + /** + * @brief The PushRuleModel that has the active connection's push rules. + */ + Q_PROPERTY(PushRuleModel *pushRuleModel READ pushRuleModel CONSTANT) + /** * @brief The row number in the accounts directory of the active connection. */ @@ -119,6 +125,8 @@ public: void setActiveConnection(Quotient::Connection *connection); [[nodiscard]] Quotient::Connection *activeConnection() const; + [[nodiscard]] PushRuleModel *pushRuleModel() const; + /** * @brief Add a new connection to the account registry. */ @@ -236,6 +244,8 @@ private: bool hasWindowSystem() const; + QPointer m_pushRuleModel; + private Q_SLOTS: void invokeLogin(); void showWindow(); diff --git a/src/main.cpp b/src/main.cpp index 9d8d97695..7ed34e273 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -57,13 +57,13 @@ #include "models/emojimodel.h" #include "models/emoticonfiltermodel.h" #include "models/imagepacksmodel.h" -#include "models/keywordnotificationrulemodel.h" #include "models/livelocationsmodel.h" #include "models/locationsmodel.h" #include "models/mediamessagefiltermodel.h" #include "models/messageeventmodel.h" #include "models/messagefiltermodel.h" #include "models/publicroomlistmodel.h" +#include "models/pushrulemodel.h" #include "models/reactionmodel.h" #include "models/roomlistmodel.h" #include "models/searchmodel.h" @@ -257,13 +257,15 @@ int main(int argc, char *argv[]) #ifdef QUOTIENT_07 qmlRegisterType("org.kde.neochat", 1, 0, "PollHandler"); #endif - qmlRegisterType("org.kde.neochat", 1, 0, "KeywordNotificationRuleModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "PushRuleModel"); qmlRegisterType("org.kde.neochat", 1, 0, "StickerModel"); qmlRegisterType("org.kde.neochat", 1, 0, "ImagePacksModel"); qmlRegisterType("org.kde.neochat", 1, 0, "AccountEmoticonModel"); qmlRegisterType("org.kde.neochat", 1, 0, "EmoticonFilterModel"); qmlRegisterType("org.kde.neochat", 1, 0, "DelegateSizeHelper"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM"); + qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM"); + qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM"); diff --git a/src/models/keywordnotificationrulemodel.cpp b/src/models/keywordnotificationrulemodel.cpp deleted file mode 100644 index e9ac61456..000000000 --- a/src/models/keywordnotificationrulemodel.cpp +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-FileCopyrightText: 2022 James Graham -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#include "keywordnotificationrulemodel.h" -#include "controller.h" -#include "notificationsmanager.h" - -#include -#include -#include -#include -#include -#include - -KeywordNotificationRuleModel::KeywordNotificationRuleModel(QObject *parent) - : QAbstractListModel(parent) -{ - if (Controller::instance().activeConnection()) { - controllerConnectionChanged(); - } - connect(&Controller::instance(), &Controller::activeConnectionChanged, this, &KeywordNotificationRuleModel::controllerConnectionChanged); -} - -void KeywordNotificationRuleModel::controllerConnectionChanged() -{ - connect(Controller::instance().activeConnection(), &Quotient::Connection::accountDataChanged, this, &KeywordNotificationRuleModel::updateNotificationRules); - updateNotificationRules("m.push_rules"); -} - -void KeywordNotificationRuleModel::updateNotificationRules(const QString &type) -{ - if (type != "m.push_rules") { - return; - } - - const QJsonObject ruleDataJson = Controller::instance().activeConnection()->accountDataJson("m.push_rules"); - const Quotient::PushRuleset ruleData = Quotient::fromJson(ruleDataJson["global"].toObject()); - const QVector contentRules = ruleData.content; - - beginResetModel(); - m_notificationRules.clear(); - for (const auto &i : contentRules) { - if (!m_notificationRules.contains(i.ruleId) && i.ruleId[0] != '.') { - m_notificationRules.append(i.ruleId); - } - } - endResetModel(); -} - -QVariant KeywordNotificationRuleModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) { - return {}; - } - - if (index.row() >= m_notificationRules.count()) { - qDebug() << "KeywordNotificationRuleModel, something's wrong: index.row() >= m_notificationRules.count()"; - return {}; - } - - if (role == NameRole) { - return m_notificationRules.at(index.row()); - } - - return {}; -} - -int KeywordNotificationRuleModel::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent) - - return m_notificationRules.count(); -} - -void KeywordNotificationRuleModel::addKeyword(const QString &keyword) -{ - if (m_notificationRules.count() == 0) { - NotificationsManager::instance().initializeKeywordNotificationAction(); - } - - const QVector actions = NotificationsManager::instance().getKeywordNotificationActions(); - - auto job = Controller::instance() - .activeConnection() - ->callApi("global", "content", keyword, actions, "", "", QVector(), keyword); - connect(job, &Quotient::BaseJob::success, this, [this, keyword]() { - beginInsertRows(QModelIndex(), m_notificationRules.count(), m_notificationRules.count()); - m_notificationRules.append(keyword); - endInsertRows(); - }); -} - -void KeywordNotificationRuleModel::removeKeywordAtIndex(int index) -{ - auto job = Controller::instance().activeConnection()->callApi("global", "content", m_notificationRules[index]); - connect(job, &Quotient::BaseJob::success, this, [this, index]() { - beginRemoveRows(QModelIndex(), index, index); - m_notificationRules.removeAt(index); - endRemoveRows(); - - if (m_notificationRules.count() == 0) { - NotificationsManager::instance().deactivateKeywordNotificationAction(); - } - }); -} - -QHash KeywordNotificationRuleModel::roleNames() const -{ - return {{NameRole, QByteArrayLiteral("name")}}; -} diff --git a/src/models/keywordnotificationrulemodel.h b/src/models/keywordnotificationrulemodel.h deleted file mode 100644 index f796a369b..000000000 --- a/src/models/keywordnotificationrulemodel.h +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2022 James Graham -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include - -#include - -/** - * @class KeywordNotificationRuleModel - * - * This class defines the model for managing notification push rule keywords. - */ -class KeywordNotificationRuleModel : public QAbstractListModel -{ - Q_OBJECT - -public: - /** - * @brief Defines the model roles. - */ - enum EventRoles { - NameRole = Qt::DisplayRole, /**< The push rule keyword. */ - }; - - KeywordNotificationRuleModel(QObject *parent = nullptr); - - /** - * @brief Get the given role value at the given index. - * - * @sa QAbstractItemModel::data - */ - [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - /** - * @brief Number of rows in the model. - * - * @sa QAbstractItemModel::rowCount - */ - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; - - /** - * @brief Returns a mapping from Role enum values to role names. - * - * @sa EventRoles, QAbstractItemModel::roleNames() - */ - [[nodiscard]] QHash roleNames() const override; - - /** - * @brief Add a new keyword to the model. - */ - Q_INVOKABLE void addKeyword(const QString &keyword); - - /** - * @brief Remove a keyword from the model. - */ - Q_INVOKABLE void removeKeywordAtIndex(int index); - -private Q_SLOTS: - void controllerConnectionChanged(); - void updateNotificationRules(const QString &type); - -private: - QList m_notificationRules; -}; diff --git a/src/models/livelocationsmodel.cpp b/src/models/livelocationsmodel.cpp index 5de4871ce..e48cf4920 100644 --- a/src/models/livelocationsmodel.cpp +++ b/src/models/livelocationsmodel.cpp @@ -4,7 +4,7 @@ #include "livelocationsmodel.h" -#include +#include #include diff --git a/src/models/pushrulemodel.cpp b/src/models/pushrulemodel.cpp new file mode 100644 index 000000000..d2c32c615 --- /dev/null +++ b/src/models/pushrulemodel.cpp @@ -0,0 +1,445 @@ +// SPDX-FileCopyrightText: 2022 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "pushrulemodel.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "controller.h" +#include "neochatconfig.h" +#include "notificationsmanager.h" + +// Alternate name text for default rules. +static const QHash defaultRuleNames = { + {QStringLiteral(".m.rule.master"), QStringLiteral("Enable notifications for this account")}, + {QStringLiteral(".m.rule.room_one_to_one"), QStringLiteral("Messages in one-to-one chats")}, + {QStringLiteral(".m.rule.encrypted_room_one_to_one"), QStringLiteral("Encrypted messages in one-to-one chats")}, + {QStringLiteral(".m.rule.message"), QStringLiteral("Messages in group chats")}, + {QStringLiteral(".m.rule.encrypted"), QStringLiteral("Messages in encrypted group chats")}, + {QStringLiteral(".m.rule.tombstone"), QStringLiteral("Room upgrade messages")}, + {QStringLiteral(".m.rule.contains_display_name"), QStringLiteral("Messages containing my display name")}, + {QStringLiteral(".m.rule.roomnotif"), QStringLiteral("Whole room (@room) notifications")}, + {QStringLiteral(".m.rule.invite_for_me"), QStringLiteral("Invites to a room")}, + {QStringLiteral(".m.rule.call"), QStringLiteral("Call invitation")}, +}; + +// Sections for default rules. +static const QHash defaultSections = { + {QStringLiteral(".m.rule.master"), PushNotificationSection::Master}, + {QStringLiteral(".m.rule.room_one_to_one"), PushNotificationSection::Room}, + {QStringLiteral(".m.rule.encrypted_room_one_to_one"), PushNotificationSection::Room}, + {QStringLiteral(".m.rule.message"), PushNotificationSection::Room}, + {QStringLiteral(".m.rule.encrypted"), PushNotificationSection::Room}, + {QStringLiteral(".m.rule.tombstone"), PushNotificationSection::Room}, + {QStringLiteral(".m.rule.contains_display_name"), PushNotificationSection::Mentions}, + {QStringLiteral(".m.rule.roomnotif"), PushNotificationSection::Mentions}, + {QStringLiteral(".m.rule.invite_for_me"), PushNotificationSection::Invites}, + {QStringLiteral(".m.rule.call"), PushNotificationSection::Undefined}, // TODO: make invites when VOIP added. + {QStringLiteral(".m.rule.suppress_notices"), PushNotificationSection::Undefined}, + {QStringLiteral(".m.rule.member_event"), PushNotificationSection::Undefined}, + {QStringLiteral(".m.rule.reaction"), PushNotificationSection::Undefined}, + {QStringLiteral(".m.rule.room.server_acl"), PushNotificationSection::Undefined}, + {QStringLiteral(".im.vector.jitsi"), PushNotificationSection::Undefined}, +}; + +// Default rules that don't have a highlight option as it would lead to all messages +// in a room being highlighted. +static const QStringList noHighlight = { + QStringLiteral(".m.rule.room_one_to_one"), + QStringLiteral(".m.rule.encrypted_room_one_to_one"), + QStringLiteral(".m.rule.message"), + QStringLiteral(".m.rule.encrypted"), +}; + +PushRuleModel::PushRuleModel(QObject *parent) + : QAbstractListModel(parent) +{ + m_defaultKeywordAction = static_cast(NeoChatConfig::self()->keywordPushRuleDefault()); + + if (Controller::instance().activeConnection()) { + controllerConnectionChanged(); + } + connect(&Controller::instance(), &Controller::activeConnectionChanged, this, &PushRuleModel::controllerConnectionChanged); +} + +void PushRuleModel::controllerConnectionChanged() +{ + connect(Controller::instance().activeConnection(), &Quotient::Connection::accountDataChanged, this, &PushRuleModel::updateNotificationRules); + updateNotificationRules("m.push_rules"); +} + +void PushRuleModel::updateNotificationRules(const QString &type) +{ + if (type != "m.push_rules") { + return; + } + + const QJsonObject ruleDataJson = Controller::instance().activeConnection()->accountDataJson("m.push_rules"); + const Quotient::PushRuleset ruleData = Quotient::fromJson(ruleDataJson["global"].toObject()); + + beginResetModel(); + m_rules.clear(); + + // Doing this 5 times because PushRuleset is a struct. + setRules(ruleData.override, PushNotificationKind::Override); + setRules(ruleData.content, PushNotificationKind::Content); + setRules(ruleData.room, PushNotificationKind::Room); + setRules(ruleData.sender, PushNotificationKind::Sender); + setRules(ruleData.underride, PushNotificationKind::Underride); + + Q_EMIT globalNotificationsEnabledChanged(); + Q_EMIT globalNotificationsSetChanged(); + + endResetModel(); +} + +void PushRuleModel::setRules(QVector rules, PushNotificationKind::Kind kind) +{ + for (const auto &rule : rules) { + QString roomId; + if (rule.conditions.size() > 0) { + for (const auto &condition : rule.conditions) { + if (condition.key == QStringLiteral("room_id")) { + roomId = condition.pattern; + } + } + } + + m_rules.append(Rule{ + rule.ruleId, + kind, + variantToAction(rule.actions, rule.enabled), + getSection(rule), + rule.enabled, + roomId, + }); + } +} + +int PushRuleModel::getRuleIndex(const QString &ruleId) const +{ + for (auto i = 0; i < m_rules.count(); i++) { + if (m_rules[i].id == ruleId) { + return i; + } + } + return -1; +} + +PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule rule) +{ + auto ruleId = rule.ruleId; + + if (defaultSections.contains(ruleId)) { + return defaultSections.value(ruleId); + } else { + /** + * If the rule name resolves to a matrix id for a room that the user is part + * of it shouldn't appear in the global list as it's overriding the global + * state for that room. + * + * Rooms that the user hasn't joined shouldn't have a rule. + */ + auto connection = Controller::instance().activeConnection(); + if (connection->room(ruleId) != nullptr) { + return PushNotificationSection::Undefined; + } + /** + * If the rule name resolves to a matrix id for a user it shouldn't appear + * in the global list as it's a rule to block notifications from a user and + * is handled elsewhere. + */ + auto testUserId = ruleId; + // Rules for user matrix IDs often don't have the @ on the beginning so add + // if not there to avoid malformed ID. + if (!testUserId.startsWith(u'@')) { + testUserId.prepend(u'@'); + } + if (connection->user(testUserId) != nullptr) { + return PushNotificationSection::Undefined; + } + // If the rule has push conditions and one is a room ID it is a room only keyword. + if (!rule.conditions.isEmpty()) { + for (auto condition : rule.conditions) { + if (condition.key == QStringLiteral("room_id")) { + return PushNotificationSection::RoomKeywords; + } + } + } + return PushNotificationSection::Keywords; + } +} + +PushNotificationAction::Action PushRuleModel::defaultState() const +{ + return m_defaultKeywordAction; +} + +void PushRuleModel::setDefaultState(PushNotificationAction::Action defaultState) +{ + if (defaultState == m_defaultKeywordAction) { + return; + } + m_defaultKeywordAction = defaultState; + NeoChatConfig::setKeywordPushRuleDefault(m_defaultKeywordAction); + Q_EMIT defaultStateChanged(); +} + +bool PushRuleModel::globalNotificationsEnabled() const +{ + auto masterIndex = getRuleIndex(QStringLiteral(".m.rule.master")); + if (masterIndex > -1) { + return !m_rules[masterIndex].enabled; + } + return false; +} + +void PushRuleModel::setGlobalNotificationsEnabled(bool enabled) +{ + setNotificationRuleEnabled("override", ".m.rule.master", !enabled); +} + +bool PushRuleModel::globalNotificationsSet() const +{ + return getRuleIndex(QStringLiteral(".m.rule.master")) > -1; +} + +QVariant PushRuleModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (index.row() >= rowCount()) { + qDebug() << "PushRuleModel, something's wrong: index.row() >= m_rules.count()"; + return {}; + } + + if (role == NameRole) { + auto ruleId = m_rules.at(index.row()).id; + if (defaultRuleNames.contains(ruleId)) { + return defaultRuleNames.value(ruleId); + } else { + return ruleId; + } + } + if (role == IdRole) { + return m_rules.at(index.row()).id; + } + if (role == KindRole) { + return m_rules.at(index.row()).kind; + } + if (role == ActionRole) { + return m_rules.at(index.row()).action; + } + if (role == HighlightableRole) { + return !noHighlight.contains(m_rules.at(index.row()).id); + } + if (role == DeletableRole) { + return !m_rules.at(index.row()).id.startsWith(QStringLiteral(".")); + } + if (role == SectionRole) { + return m_rules.at(index.row()).section; + } + if (role == RoomIdRole) { + return m_rules.at(index.row()).roomId; + } + + return {}; +} + +int PushRuleModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return m_rules.count(); +} + +QHash PushRuleModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles[NameRole] = "name"; + roles[IdRole] = "id"; + roles[KindRole] = "kind"; + roles[ActionRole] = "ruleAction"; + roles[HighlightableRole] = "highlightable"; + roles[DeletableRole] = "deletable"; + roles[SectionRole] = "section"; + roles[RoomIdRole] = "roomId"; + return roles; +} + +void PushRuleModel::setPushRuleAction(const QString &id, PushNotificationAction::Action action) +{ + int index = getRuleIndex(id); + if (index == -1) { + return; + } + + auto rule = m_rules[index]; + + // Override rules need to be disabled when off so that other rules can match the message if they apply. + if (action == PushNotificationAction::Off && rule.kind == PushNotificationKind::Override) { + setNotificationRuleEnabled(PushNotificationKind::kindString(rule.kind), rule.id, false); + } else if (rule.kind == PushNotificationKind::Override) { + setNotificationRuleEnabled(PushNotificationKind::kindString(rule.kind), rule.id, true); + } + + setNotificationRuleActions(PushNotificationKind::kindString(rule.kind), rule.id, action); +} + +void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId) +{ + PushNotificationKind::Kind kind = PushNotificationKind::Content; + const QVector actions = actionToVariant(m_defaultKeywordAction); + QVector pushConditions; + if (!roomId.isEmpty()) { + kind = PushNotificationKind::Override; + + Quotient::PushCondition roomCondition; + roomCondition.kind = "event_match"; + roomCondition.key = "room_id"; + roomCondition.pattern = roomId; + pushConditions.append(roomCondition); + + Quotient::PushCondition keywordCondition; + keywordCondition.kind = "event_match"; + keywordCondition.key = "content.body"; + keywordCondition.pattern = keyword; + pushConditions.append(keywordCondition); + } + + auto job = Controller::instance().activeConnection()->callApi("global", + PushNotificationKind::kindString(kind), + keyword, + actions, + QLatin1String(""), + QLatin1String(""), + pushConditions, + roomId.isEmpty() ? keyword : QLatin1String("")); + connect(job, &Quotient::BaseJob::failure, this, [job, keyword]() { + qWarning() << QLatin1String("Unable to set push rule for keyword %1: ").arg(keyword) << job->errorString(); + }); +} + +/** + * The rule never being removed from the list by this function is intentional. When + * the server is updated the new push rule account data will be synced and it will + * be removed when the model is updated then. + */ +void PushRuleModel::removeKeyword(const QString &keyword) +{ + int index = getRuleIndex(keyword); + if (index == -1) { + return; + } + + auto kind = PushNotificationKind::kindString(m_rules[index].kind); + auto job = Controller::instance().activeConnection()->callApi("global", kind, m_rules[index].id); + connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() { + qWarning() << QLatin1String("Unable to remove push rule for keyword %1: ").arg(m_rules[index].id) << job->errorString(); + }); +} + +void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled) +{ + auto job = Controller::instance().activeConnection()->callApi("global", kind, ruleId); + connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled]() { + if (job->enabled() != enabled) { + Controller::instance().activeConnection()->callApi("global", kind, ruleId, enabled); + } + }); +} + +void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action) +{ + QVector actions; + if (ruleId == ".m.rule.call") { + actions = actionToVariant(action, "ring"); + } else { + actions = actionToVariant(action); + } + + Controller::instance().activeConnection()->callApi("global", kind, ruleId, actions); +} + +PushNotificationAction::Action PushRuleModel::variantToAction(const QVector &actions, bool enabled) +{ + bool notify = false; + bool isNoisy = false; + bool highlightEnabled = false; + for (const auto &i : actions) { + auto actionString = i.toString(); + if (!actionString.isEmpty()) { + if (actionString == QLatin1String("notify")) { + notify = true; + } + continue; + } + + QJsonObject action = i.toJsonObject(); + if (action["set_tweak"].toString() == "sound") { + isNoisy = true; + } else if (action["set_tweak"].toString() == "highlight") { + if (action["value"].toString() != "false") { + highlightEnabled = true; + } + } + } + + if (!enabled) { + return PushNotificationAction::Off; + } + + if (notify) { + if (isNoisy && highlightEnabled) { + return PushNotificationAction::NoisyHighlight; + } else if (isNoisy) { + return PushNotificationAction::Noisy; + } else if (highlightEnabled) { + return PushNotificationAction::Highlight; + } else { + return PushNotificationAction::On; + } + } else { + return PushNotificationAction::Off; + } +} + +QVector PushRuleModel::actionToVariant(PushNotificationAction::Action action, const QString &sound) +{ + // 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 (action == PushNotificationAction::Unknown) { + Q_ASSERT(false); + return QVector(); + } + + QVector actions; + + if (action != PushNotificationAction::Off) { + actions.append("notify"); + } else { + actions.append("dont_notify"); + } + if (action == PushNotificationAction::Noisy || action == PushNotificationAction::NoisyHighlight) { + QJsonObject soundTweak; + soundTweak.insert("set_tweak", "sound"); + soundTweak.insert("value", sound); + actions.append(soundTweak); + } + if (action == PushNotificationAction::Highlight || action == PushNotificationAction::NoisyHighlight) { + QJsonObject highlightTweak; + highlightTweak.insert("set_tweak", "highlight"); + actions.append(highlightTweak); + } + + return actions; +} diff --git a/src/models/pushrulemodel.h b/src/models/pushrulemodel.h new file mode 100644 index 000000000..c7ebee2c2 --- /dev/null +++ b/src/models/pushrulemodel.h @@ -0,0 +1,241 @@ +// SPDX-FileCopyrightText: 2022 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include + +#include + +#include "notificationsmanager.h" + +/** + * @class PushNotificationKind + * + * A class with the Kind enum for push notifications and helper functions. + * + * The kind relates to the kinds of push rule definied in the matrix spec, see + * https://spec.matrix.org/v1.7/client-server-api/#push-rules for full details. + */ +class PushNotificationKind : public QObject +{ + Q_OBJECT + +public: + /** + * @brief Defines the different kinds of push rule. + */ + enum Kind { + Override = 0, /**< The highest priority rules. */ + Content, /**< These configure behaviour for messages that match certain patterns. */ + Room, /**< These rules change the behaviour of all messages for a given room. */ + Sender, /**< These rules configure notification behaviour for messages from a specific Matrix user ID. */ + Underride, /**< These are identical to override rules, but have a lower priority than content, room and sender rules. */ + }; + Q_ENUM(Kind); + + /** + * @brief Translate the Kind enum value to a human readable string. + * + * @sa Kind + */ + static QString kindString(Kind kind) + { + switch (kind) { + case Kind::Override: + return QLatin1String("override"); + case Kind::Content: + return QLatin1String("content"); + case Kind::Room: + return QLatin1String("room"); + case Kind::Sender: + return QLatin1String("sender"); + case Kind::Underride: + return QLatin1String("underride"); + default: + return {}; + } + }; +}; + +/** + * @class PushNotificationSection + * + * A class with the Section enum for push notifications and helper functions. + * + * @note This is different from the PushNotificationKind and instead is used for sorting + * in the settings page which is not necessarily by Kind. + * + * @sa PushNotificationKind + */ +class PushNotificationSection : public QObject +{ + Q_OBJECT + +public: + /** + * @brief Defines the sections to sort push rules into. + */ + enum Section { + Master = 0, /**< The master push rule */ + Room, /**< Push rules relating to all rooms. */ + Mentions, /**< Push rules relating to user mentions. */ + Keywords, /**< Global Keyword push rules. */ + RoomKeywords, /**< Keyword push rules that only apply to a specific room. */ + Invites, /**< Push rules relating to invites. */ + /** + * @brief Push rules that should never be shown. + * + * There are numerous rules that get set that shouldn't be shown in the general + * list e.g. The array of rules used to override global settings in individual + * rooms. + */ + Undefined, + }; + Q_ENUM(Section); + + /** + * @brief Translate the Section enum value to a human readable string. + * + * @sa Section + */ + static QString sectionString(Section section) + { + switch (section) { + case Section::Master: + return QLatin1String("Master"); + case Section::Room: + return QLatin1String("Room Notifications"); + case Section::Mentions: + return QLatin1String("@Mentions"); + case Section::Keywords: + return QLatin1String("Keywords"); + case Section::Invites: + return QLatin1String("Invites"); + default: + return {}; + } + }; +}; + +/** + * @class PushRuleModel + * + * This class defines the model for managing notification push rule keywords. + */ +class PushRuleModel : public QAbstractListModel +{ + Q_OBJECT + + /** + * @brief The default state for any newly created keyword rule. + */ + Q_PROPERTY(PushNotificationAction::Action defaultState READ defaultState WRITE setDefaultState NOTIFY defaultStateChanged) + + /** + * @brief The global notification state. + * + * If this rule is set to off all push notifications are disabled regardless + * of other settings. + */ + Q_PROPERTY(bool globalNotificationsEnabled READ globalNotificationsEnabled WRITE setGlobalNotificationsEnabled NOTIFY globalNotificationsEnabledChanged) + + /** + * @brief Whether the global notification state has been retrieved from the server. + * + * @sa globalNotificationsEnabled, PushNotificationAction::Action + */ + Q_PROPERTY(bool globalNotificationsSet READ globalNotificationsSet NOTIFY globalNotificationsSetChanged) + +public: + struct Rule { + QString id; + PushNotificationKind::Kind kind; + PushNotificationAction::Action action; + PushNotificationSection::Section section; + bool enabled; + QString roomId; + }; + + /** + * @brief Defines the model roles. + */ + enum EventRoles { + NameRole = Qt::DisplayRole, /**< The push rule name. */ + IdRole, /**< The push rule ID. */ + KindRole, /**< The kind of notification rule; override, content, etc. */ + ActionRole, /**< The PushNotificationAction for the rule. */ + HighlightableRole, /**< Whether the rule can have a highlight action. */ + DeletableRole, /**< Whether the rule can be deleted the rule. */ + SectionRole, /**< The section to sort into in the settings page. */ + RoomIdRole, /**< The room the rule applies to (blank if global). */ + }; + Q_ENUM(EventRoles) + + PushRuleModel(QObject *parent = nullptr); + + [[nodiscard]] PushNotificationAction::Action defaultState() const; + void setDefaultState(PushNotificationAction::Action defaultState); + + [[nodiscard]] bool globalNotificationsEnabled() const; + void setGlobalNotificationsEnabled(bool enabled); + + [[nodiscard]] bool globalNotificationsSet() const; + + /** + * @brief Get the given role value at the given index. + * + * @sa QAbstractItemModel::data + */ + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + /** + * @brief Number of rows in the model. + * + * @sa QAbstractItemModel::rowCount + */ + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + /** + * @brief Returns a mapping from Role enum values to role names. + * + * @sa EventRoles, QAbstractItemModel::roleNames() + */ + [[nodiscard]] QHash roleNames() const override; + + Q_INVOKABLE void setPushRuleAction(const QString &id, PushNotificationAction::Action action); + + /** + * @brief Add a new keyword to the model. + */ + Q_INVOKABLE void addKeyword(const QString &keyword, const QString &roomId = {}); + + /** + * @brief Remove a keyword from the model. + */ + Q_INVOKABLE void removeKeyword(const QString &keyword); + +Q_SIGNALS: + void defaultStateChanged(); + void globalNotificationsEnabledChanged(); + void globalNotificationsSetChanged(); + +private Q_SLOTS: + void controllerConnectionChanged(); + void updateNotificationRules(const QString &type); + +private: + PushNotificationAction::Action m_defaultKeywordAction; + QList m_rules; + + void setRules(QVector rules, PushNotificationKind::Kind kind); + + int getRuleIndex(const QString &ruleId) const; + PushNotificationSection::Section getSection(Quotient::PushRule rule); + + void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled); + void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action); + PushNotificationAction::Action variantToAction(const QVector &actions, bool enabled); + QVector actionToVariant(PushNotificationAction::Action action, const QString &sound = "default"); +}; +Q_DECLARE_METATYPE(PushRuleModel *) diff --git a/src/neochatconfig.kcfg b/src/neochatconfig.kcfg index 0969944a2..c1a33da08 100644 --- a/src/neochatconfig.kcfg +++ b/src/neochatconfig.kcfg @@ -54,6 +54,10 @@ + + + 4 + diff --git a/src/notificationsmanager.cpp b/src/notificationsmanager.cpp index 5f96467b8..dc7f86e03 100644 --- a/src/notificationsmanager.cpp +++ b/src/notificationsmanager.cpp @@ -41,12 +41,6 @@ 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::updateNotificationRules); - // Ensure that the push rule states are retrieved after the connection is changed - updateNotificationRules("m.push_rules"); - }); } #ifdef QUOTIENT_07 @@ -300,309 +294,3 @@ 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) -{ - setNotificationRuleEnabled("override", ".m.rule.master", !enabled); -} - -void NotificationsManager::setOneToOneNotificationAction(PushNotificationAction::Action action) -{ - setNotificationRuleActions("underride", ".m.rule.room_one_to_one", action); -} - -void NotificationsManager::setEncryptedOneToOneNotificationAction(PushNotificationAction::Action action) -{ - setNotificationRuleActions("underride", ".m.rule.encrypted_room_one_to_one", action); -} - -void NotificationsManager::setGroupChatNotificationAction(PushNotificationAction::Action action) -{ - setNotificationRuleActions("underride", ".m.rule.message", action); -} - -void NotificationsManager::setEncryptedGroupChatNotificationAction(PushNotificationAction::Action action) -{ - setNotificationRuleActions("underride", ".m.rule.encrypted", action); -} - -/* - * .m.rule.contains_display_name is an override rule so it needs to be disabled when off - * so that other rules can match the message if they apply. - */ -void NotificationsManager::setDisplayNameNotificationAction(PushNotificationAction::Action action) -{ - if (action == PushNotificationAction::Off) { - setNotificationRuleEnabled("override", ".m.rule.contains_display_name", false); - } else { - setNotificationRuleActions("override", ".m.rule.contains_display_name", action); - setNotificationRuleEnabled("override", ".m.rule.contains_display_name", true); - } -} - -/* - * .m.rule.roomnotif is an override rule so it needs to be disabled when off - * so that other rules can match the message if they apply. - */ -void NotificationsManager::setRoomNotificationAction(PushNotificationAction::Action action) -{ - if (action == PushNotificationAction::Off) { - setNotificationRuleEnabled("override", ".m.rule.roomnotif", false); - } else { - setNotificationRuleActions("override", ".m.rule.roomnotif", action); - setNotificationRuleEnabled("override", ".m.rule.roomnotif", true); - } -} - -void NotificationsManager::initializeKeywordNotificationAction() -{ - m_keywordNotificationAction = PushNotificationAction::Highlight; - Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction); -} - -void NotificationsManager::deactivateKeywordNotificationAction() -{ - m_keywordNotificationAction = PushNotificationAction::Off; - Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction); -} - -QVector NotificationsManager::getKeywordNotificationActions() -{ - return toActions(m_keywordNotificationAction); -} - -void NotificationsManager::setKeywordNotificationAction(PushNotificationAction::Action action) -{ - // Unlike the other rules this needs to be set here for the case where there are no keyords. - m_keywordNotificationAction = action; - Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction); - - const QJsonObject accountData = Controller::instance().activeConnection()->accountDataJson("m.push_rules"); - const QJsonArray contentRuleArray = accountData["global"].toObject()["content"].toArray(); - for (const auto &i : contentRuleArray) { - const QJsonObject contentRule = i.toObject(); - if (contentRule["rule_id"].toString()[0] != '.') { - setNotificationRuleActions("content", contentRule["rule_id"].toString(), action); - } - } -} - -/* - * .m.rule.invite_for_me is an override rule so it needs to be disabled when off - * so that other rules can match the message if they apply. - */ -void NotificationsManager::setInviteNotificationAction(PushNotificationAction::Action action) -{ - if (action == PushNotificationAction::Off) { - setNotificationRuleEnabled("override", ".m.rule.invite_for_me", false); - } else { - setNotificationRuleActions("override", ".m.rule.invite_for_me", action); - setNotificationRuleEnabled("override", ".m.rule.invite_for_me", true); - } -} - -void NotificationsManager::setCallInviteNotificationAction(PushNotificationAction::Action action) -{ - setNotificationRuleActions("underride", ".m.rule.call", action); -} - -/* - * .m.rule.tombstone is an override rule so it needs to be disabled when off - * so that other rules can match the message if they apply. - */ -void NotificationsManager::setTombstoneNotificationAction(PushNotificationAction::Action action) -{ - if (action == PushNotificationAction::Off) { - setNotificationRuleEnabled("override", ".m.rule.tombstone", false); - } else { - setNotificationRuleActions("override", ".m.rule.tombstone", action); - setNotificationRuleEnabled("override", ".m.rule.tombstone", true); - } -} - -void NotificationsManager::updateNotificationRules(const QString &type) -{ - if (type != "m.push_rules") { - return; - } - - if (!Controller::instance().activeConnection()) { - return; - } - - const QJsonObject accountData = Controller::instance().activeConnection()->accountDataJson("m.push_rules"); - - // Update override rules - const QJsonArray overrideRuleArray = accountData["global"].toObject()["override"].toArray(); - for (const auto &i : overrideRuleArray) { - const QJsonObject overrideRule = i.toObject(); - if (overrideRule["rule_id"] == ".m.rule.master") { - bool ruleEnabled = overrideRule["enabled"].toBool(); - m_globalNotificationsEnabled = !ruleEnabled; - if (!m_globalNotificationsSet) { - m_globalNotificationsSet = true; - } - Q_EMIT globalNotificationsEnabledChanged(m_globalNotificationsEnabled); - } - - const PushNotificationAction::Action action = toAction(overrideRule); - - if (overrideRule["rule_id"] == ".m.rule.contains_display_name") { - m_displayNameNotificationAction = action; - Q_EMIT displayNameNotificationActionChanged(m_displayNameNotificationAction); - } else if (overrideRule["rule_id"] == ".m.rule.roomnotif") { - m_roomNotificationAction = action; - Q_EMIT roomNotificationActionChanged(m_roomNotificationAction); - } else if (overrideRule["rule_id"] == ".m.rule.invite_for_me") { - m_inviteNotificationAction = action; - Q_EMIT inviteNotificationActionChanged(m_inviteNotificationAction); - } else if (overrideRule["rule_id"] == ".m.rule.tombstone") { - m_tombstoneNotificationAction = action; - Q_EMIT tombstoneNotificationActionChanged(m_tombstoneNotificationAction); - } - } - - // Update content rules - const QJsonArray contentRuleArray = accountData["global"].toObject()["content"].toArray(); - PushNotificationAction::Action keywordAction = PushNotificationAction::Unknown; - for (const auto &i : contentRuleArray) { - const QJsonObject contentRule = i.toObject(); - const PushNotificationAction::Action action = toAction(contentRule); - bool actionMismatch = false; - - if (contentRule["rule_id"].toString()[0] != '.' && !actionMismatch) { - if (keywordAction == PushNotificationAction::Unknown) { - keywordAction = action; - m_keywordNotificationAction = action; - Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction); - } else if (action != keywordAction) { - actionMismatch = true; - m_keywordNotificationAction = PushNotificationAction::On; - Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction); - } - } - } - // If there are no keywords set the state to off, this is the only time it'll be in the off state - if (keywordAction == PushNotificationAction::Unknown) { - m_keywordNotificationAction = PushNotificationAction::Off; - Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction); - } - - // Update underride rules - const QJsonArray underrideRuleArray = accountData["global"].toObject()["underride"].toArray(); - for (const auto &i : underrideRuleArray) { - const QJsonObject underrideRule = i.toObject(); - const PushNotificationAction::Action action = toAction(underrideRule); - - if (underrideRule["rule_id"] == ".m.rule.room_one_to_one") { - m_oneToOneNotificationAction = action; - Q_EMIT oneToOneNotificationActionChanged(m_oneToOneNotificationAction); - } else if (underrideRule["rule_id"] == ".m.rule.encrypted_room_one_to_one") { - m_encryptedOneToOneNotificationAction = action; - Q_EMIT encryptedOneToOneNotificationActionChanged(m_encryptedOneToOneNotificationAction); - } else if (underrideRule["rule_id"] == ".m.rule.message") { - m_groupChatNotificationAction = action; - Q_EMIT groupChatNotificationActionChanged(m_groupChatNotificationAction); - } else if (underrideRule["rule_id"] == ".m.rule.encrypted") { - m_encryptedGroupChatNotificationAction = action; - Q_EMIT encryptedGroupChatNotificationActionChanged(m_encryptedGroupChatNotificationAction); - } else if (underrideRule["rule_id"] == ".m.rule.call") { - m_callInviteNotificationAction = action; - Q_EMIT callInviteNotificationActionChanged(m_callInviteNotificationAction); - } - } -} - -void NotificationsManager::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled) -{ - auto job = Controller::instance().activeConnection()->callApi("global", kind, ruleId); - connect(job, &BaseJob::success, this, [job, kind, ruleId, enabled]() { - if (job->enabled() != enabled) { - Controller::instance().activeConnection()->callApi("global", kind, ruleId, enabled); - } - }); -} - -void NotificationsManager::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action) -{ - QVector actions; - if (ruleId == ".m.rule.call") { - actions = toActions(action, "ring"); - } else { - actions = toActions(action); - } - - Controller::instance().activeConnection()->callApi("global", kind, ruleId, actions); -} - -PushNotificationAction::Action NotificationsManager::toAction(const QJsonObject &rule) -{ - const QJsonArray actions = rule["actions"].toArray(); - bool isNoisy = false; - bool highlightEnabled = false; - const bool enabled = rule["enabled"].toBool(); - for (const auto &i : actions) { - QJsonObject action = i.toObject(); - if (action["set_tweak"].toString() == "sound") { - isNoisy = true; - } else if (action["set_tweak"].toString() == "highlight") { - if (action["value"].toString() != "false") { - highlightEnabled = true; - } - } - } - - if (!enabled) { - return PushNotificationAction::Off; - } - - if (actions[0] == "notify") { - if (isNoisy && highlightEnabled) { - return PushNotificationAction::NoisyHighlight; - } else if (isNoisy) { - return PushNotificationAction::Noisy; - } else if (highlightEnabled) { - return PushNotificationAction::Highlight; - } else { - return PushNotificationAction::On; - } - } else { - return PushNotificationAction::Off; - } -} - -QVector NotificationsManager::toActions(PushNotificationAction::Action action, const QString &sound) -{ - // 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 (action == PushNotificationAction::Unknown) { - Q_ASSERT(false); - return QVector(); - } - - QVector actions; - - if (action != PushNotificationAction::Off) { - actions.append("notify"); - } else { - actions.append("dont_notify"); - } - if (action == PushNotificationAction::Noisy || action == PushNotificationAction::NoisyHighlight) { - QJsonObject soundTweak; - soundTweak.insert("set_tweak", "sound"); - soundTweak.insert("value", sound); - actions.append(soundTweak); - } - if (action == PushNotificationAction::Highlight || action == PushNotificationAction::NoisyHighlight) { - QJsonObject highlightTweak; - highlightTweak.insert("set_tweak", "highlight"); - actions.append(highlightTweak); - } - - return actions; -} diff --git a/src/notificationsmanager.h b/src/notificationsmanager.h index dd4fb33be..414ce8d5d 100644 --- a/src/notificationsmanager.h +++ b/src/notificationsmanager.h @@ -56,93 +56,6 @@ class NotificationsManager : public QObject { Q_OBJECT - /** - * @brief The global notification state. - * - * If this rule is set to off all push notifications are disabled regardless - * of other settings. - */ - Q_PROPERTY(bool globalNotificationsEnabled MEMBER m_globalNotificationsEnabled WRITE setGlobalNotificationsEnabled NOTIFY globalNotificationsEnabledChanged) - - /** - * @brief Whether the global notification state has been retrieved from the server. - * - * This is different to the others below as globalNotificationsEnabled is only - * a bool rather than a PushNotificationAction::Action so a separate property - * is required to track if the state has been retrieved from the server. - * - * @sa globalNotificationsEnabled, PushNotificationAction::Action - */ - Q_PROPERTY(bool globalNotificationsSet MEMBER m_globalNotificationsSet NOTIFY globalNotificationsSetChanged) - - /** - * @brief The notification action for direct chats. - * - * @note Encrypted direct chats have a separate setting. - * - * @sa encryptedOneToOneNotificationAction - */ - Q_PROPERTY(PushNotificationAction::Action oneToOneNotificationAction MEMBER m_oneToOneNotificationAction WRITE setOneToOneNotificationAction NOTIFY - oneToOneNotificationActionChanged) - - /** - * @brief The notification action for encrypted direct chats. - */ - Q_PROPERTY(PushNotificationAction::Action encryptedOneToOneNotificationAction MEMBER m_encryptedOneToOneNotificationAction WRITE - setEncryptedOneToOneNotificationAction NOTIFY encryptedOneToOneNotificationActionChanged) - - /** - * @brief The notification action for group chats. - * - * @note Encrypted group chats have a separate setting. - * - * @sa encryptedGroupChatNotificationAction - */ - Q_PROPERTY(PushNotificationAction::Action groupChatNotificationAction MEMBER m_groupChatNotificationAction WRITE setGroupChatNotificationAction NOTIFY - groupChatNotificationActionChanged) - - /** - * @brief The notification action for encrypted group chats. - */ - Q_PROPERTY(PushNotificationAction::Action encryptedGroupChatNotificationAction MEMBER m_encryptedGroupChatNotificationAction WRITE - setEncryptedGroupChatNotificationAction NOTIFY encryptedGroupChatNotificationActionChanged) - - /** - * @brief The notification action for events containing the local user's display name. - */ - Q_PROPERTY(PushNotificationAction::Action displayNameNotificationAction MEMBER m_displayNameNotificationAction WRITE setDisplayNameNotificationAction NOTIFY - displayNameNotificationActionChanged) - - /** - * @brief The notification action for room events. - */ - Q_PROPERTY(PushNotificationAction::Action roomNotificationAction MEMBER m_roomNotificationAction WRITE setRoomNotificationAction NOTIFY - roomNotificationActionChanged) - - /** - * @brief The notification action for keyword push rules. - */ - Q_PROPERTY(PushNotificationAction::Action keywordNotificationAction MEMBER m_keywordNotificationAction WRITE setKeywordNotificationAction NOTIFY - keywordNotificationActionChanged) - - /** - * @brief The notification action for invites to chats. - */ - Q_PROPERTY(PushNotificationAction::Action inviteNotificationAction MEMBER m_inviteNotificationAction WRITE setInviteNotificationAction NOTIFY - inviteNotificationActionChanged) - - /** - * @brief The notification action for voice calls. - */ - Q_PROPERTY(PushNotificationAction::Action callInviteNotificationAction MEMBER m_callInviteNotificationAction WRITE setCallInviteNotificationAction NOTIFY - callInviteNotificationActionChanged) - - /** - * @brief The notification action for room upgrade events. - */ - Q_PROPERTY(PushNotificationAction::Action tombstoneNotificationAction MEMBER m_tombstoneNotificationAction WRITE setTombstoneNotificationAction NOTIFY - tombstoneNotificationActionChanged) - public: static NotificationsManager &instance(); @@ -164,30 +77,6 @@ public: */ void clearInvitationNotification(const QString &roomId); - /** - * @brief Set the initial keyword notification action. - * - * This is only required if no keyword rules exist and one is created. The default - * action is PushNotificationAction::Action::Highlight. - * - * @sa PushNotificationAction::Action - */ - void initializeKeywordNotificationAction(); - - /** - * @brief Set the keyword notification action to PushNotificationAction::Action::Off. - * - * This is only required the last keyword rule is deleted. - * - * @sa PushNotificationAction::Action - */ - void deactivateKeywordNotificationAction(); - - /** - * @brief Return the keyword notification action in a form required for the server push rule. - */ - QVector getKeywordNotificationActions(); - #ifdef QUOTIENT_07 /** * @brief Handle the notifications for the given connection. @@ -208,52 +97,6 @@ private: QHash m_notifications; QHash> m_invitations; - bool m_globalNotificationsEnabled = false; - bool m_globalNotificationsSet = false; - PushNotificationAction::Action m_oneToOneNotificationAction = PushNotificationAction::Unknown; - PushNotificationAction::Action m_encryptedOneToOneNotificationAction = PushNotificationAction::Unknown; - PushNotificationAction::Action m_groupChatNotificationAction = PushNotificationAction::Unknown; - PushNotificationAction::Action m_encryptedGroupChatNotificationAction = PushNotificationAction::Unknown; - PushNotificationAction::Action m_displayNameNotificationAction = PushNotificationAction::Unknown; - PushNotificationAction::Action m_roomNotificationAction = PushNotificationAction::Unknown; - PushNotificationAction::Action m_keywordNotificationAction = PushNotificationAction::Unknown; - PushNotificationAction::Action m_inviteNotificationAction = PushNotificationAction::Unknown; - PushNotificationAction::Action m_callInviteNotificationAction = PushNotificationAction::Unknown; - PushNotificationAction::Action m_tombstoneNotificationAction = PushNotificationAction::Unknown; - - void setGlobalNotificationsEnabled(bool enabled); - void setOneToOneNotificationAction(PushNotificationAction::Action action); - void setEncryptedOneToOneNotificationAction(PushNotificationAction::Action action); - void setGroupChatNotificationAction(PushNotificationAction::Action action); - void setEncryptedGroupChatNotificationAction(PushNotificationAction::Action action); - void setDisplayNameNotificationAction(PushNotificationAction::Action action); - void setRoomNotificationAction(PushNotificationAction::Action action); - void setKeywordNotificationAction(PushNotificationAction::Action action); - void setInviteNotificationAction(PushNotificationAction::Action action); - void setCallInviteNotificationAction(PushNotificationAction::Action action); - void setTombstoneNotificationAction(PushNotificationAction::Action action); - - void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled); - void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action); - PushNotificationAction::Action toAction(const QJsonObject &rule); - QVector toActions(PushNotificationAction::Action action, const QString &sound = "default"); - private Q_SLOTS: void processNotificationJob(QPointer connection, Quotient::GetNotificationsJob *job, bool initialization); - - void updateNotificationRules(const QString &type); - -Q_SIGNALS: - void globalNotificationsEnabledChanged(bool newState); - void globalNotificationsSetChanged(bool newState); - void oneToOneNotificationActionChanged(PushNotificationAction::Action action); - void encryptedOneToOneNotificationActionChanged(PushNotificationAction::Action action); - void groupChatNotificationActionChanged(PushNotificationAction::Action action); - void encryptedGroupChatNotificationActionChanged(PushNotificationAction::Action action); - void displayNameNotificationActionChanged(PushNotificationAction::Action action); - void roomNotificationActionChanged(PushNotificationAction::Action action); - void keywordNotificationActionChanged(PushNotificationAction::Action action); - void inviteNotificationActionChanged(PushNotificationAction::Action action); - void callInviteNotificationActionChanged(PushNotificationAction::Action action); - void tombstoneNotificationActionChanged(PushNotificationAction::Action action); }; diff --git a/src/qml/RoomSettings/PushNotification.qml b/src/qml/RoomSettings/PushNotification.qml index cc4b454b3..f42be80a6 100644 --- a/src/qml/RoomSettings/PushNotification.qml +++ b/src/qml/RoomSettings/PushNotification.qml @@ -7,10 +7,12 @@ import QtQuick.Layouts 1.15 import org.kde.kirigami 2.15 as Kirigami import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import org.kde.kitemmodels 1.0 import org.kde.neochat 1.0 Kirigami.ScrollablePage { + id: root property NeoChatRoom room @@ -62,5 +64,86 @@ Kirigami.ScrollablePage { } } } + + MobileForm.FormCard { + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Keywords") + } + Repeater { + model: KSortFilterProxyModel { + sourceModel: Controller.pushRuleModel + + filterRowCallback: function(source_row, source_parent) { + let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) + let roomIdRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.RoomIdRole) + return sectionRole == PushNotificationSection.RoomKeywords && roomIdRole == root.room.id; + } + } + + delegate: ruleDelegate + } + MobileForm.AbstractFormDelegate { + Layout.fillWidth: true + + contentItem : RowLayout { + Kirigami.ActionTextField { + id: keywordAddField + + Layout.fillWidth: true + + placeholderText: i18n("Keyword…") + enabled: NotificationsManager.keywordNotificationAction !== PushNotificationAction.Unknown + + rightActions: Kirigami.Action { + icon.name: "edit-clear" + visible: keywordAddField.text.length > 0 + onTriggered: { + keywordAddField.text = "" + } + } + + onAccepted: { + Controller.pushRuleModel.addKeyword(keywordAddField.text, root.room.id) + keywordAddField.text = "" + } + } + QQC2.Button { + id: addButton + + text: i18n("Add keyword") + Accessible.name: text + icon.name: "list-add" + display: QQC2.AbstractButton.IconOnly + enabled: NotificationsManager.keywordNotificationAction !== PushNotificationAction.Unknown + + onClicked: { + Controller.pushRuleModel.addKeyword(keywordAddField.text, root.room.id) + keywordAddField.text = "" + } + + QQC2.ToolTip { + text: addButton.text + delay: Kirigami.Units.toolTipDelay + } + } + } + } + } + } + } + + Component { + id: ruleDelegate + NotificationRuleItem { + onDeleteRule: { + Controller.pushRuleModel.removeKeyword(id) + } + onActionChanged: (action) => Controller.pushRuleModel.setPushRuleAction(id, action) + } } } diff --git a/src/qml/Settings/GlobalNotificationsPage.qml b/src/qml/Settings/GlobalNotificationsPage.qml index 0cb040840..8816a5d49 100644 --- a/src/qml/Settings/GlobalNotificationsPage.qml +++ b/src/qml/Settings/GlobalNotificationsPage.qml @@ -7,13 +7,17 @@ import QtQuick.Layouts 1.15 import org.kde.kirigami 2.15 as Kirigami import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import org.kde.kitemmodels 1.0 import org.kde.neochat 1.0 Kirigami.ScrollablePage { + id: root + title: i18nc("@title:window", "Notifications") leftPadding: 0 rightPadding: 0 + ColumnLayout { id: notificationLayout @@ -22,10 +26,10 @@ Kirigami.ScrollablePage { contentItem: MobileForm.FormCheckDelegate { text: i18n("Enable notifications for this account") description: i18n("Whether push notifications are generated by your Matrix server") - checked: NotificationsManager.globalNotificationsEnabled - enabled: NotificationsManager.globalNotificationsSet + checked: Controller.pushRuleModel.globalNotificationsEnabled + enabled: Controller.pushRuleModel.globalNotificationsSet onToggled: { - NotificationsManager.globalNotificationsEnabled = checked + Controller.pushRuleModel.globalNotificationsEnabled = checked } } } @@ -39,81 +43,17 @@ Kirigami.ScrollablePage { MobileForm.FormCardHeader { title: i18n("Room Notifications") } - NotificationRuleItem { - text: i18n("Messages in one-to-one chats") + Repeater { + model: KSortFilterProxyModel { + sourceModel: Controller.pushRuleModel - notificationsOn: notificationLayout.isNotificationRuleOn(NotificationsManager.oneToOneNotificationAction) - noisyOn: notificationLayout.isNotificationRuleNoisy(NotificationsManager.oneToOneNotificationAction) - enabled: NotificationsManager.oneToOneNotificationAction !== PushNotificationAction.Unknown - - notificationAction: NotificationsManager.oneToOneNotificationAction - onNotificationActionChanged: { - if (notificationAction && NotificationsManager.oneToOneNotificationAction != notificationAction) { - NotificationsManager.oneToOneNotificationAction = notificationAction + filterRowCallback: function(source_row, source_parent) { + let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) + return sectionRole == PushNotificationSection.Room; } } - } - NotificationRuleItem { - text: i18n("Encrypted messages in one-to-one chats") - visible: Controller.encryptionSupported - - notificationsOn: notificationLayout.isNotificationRuleOn(NotificationsManager.encryptedOneToOneNotificationAction) - noisyOn: notificationLayout.isNotificationRuleNoisy(NotificationsManager.encryptedOneToOneNotificationAction) - enabled: NotificationsManager.encryptedOneToOneNotificationAction !== PushNotificationAction.Unknown - - notificationAction: NotificationsManager.encryptedOneToOneNotificationAction - onNotificationActionChanged: { - if (notificationAction && NotificationsManager.encryptedOneToOneNotificationAction != notificationAction) { - NotificationsManager.encryptedOneToOneNotificationAction = notificationAction - } - } - } - NotificationRuleItem { - text: i18n("Messages in group chats") - - notificationsOn: notificationLayout.isNotificationRuleOn(NotificationsManager.groupChatNotificationAction) - noisyOn: notificationLayout.isNotificationRuleNoisy(NotificationsManager.groupChatNotificationAction) - enabled: NotificationsManager.groupChatNotificationAction !== PushNotificationAction.Unknown - - notificationAction: NotificationsManager.groupChatNotificationAction - onNotificationActionChanged: { - if (notificationAction && NotificationsManager.groupChatNotificationAction != notificationAction) { - NotificationsManager.groupChatNotificationAction = notificationAction - } - } - } - NotificationRuleItem { - text: i18n("Messages in encrypted group chats") - - visible: Controller.encryptionSupported - - notificationsOn: notificationLayout.isNotificationRuleOn(NotificationsManager.encryptedGroupChatNotificationAction) - noisyOn: notificationLayout.isNotificationRuleNoisy(NotificationsManager.encryptedGroupChatNotificationAction) - enabled: NotificationsManager.encryptedGroupChatNotificationAction !== PushNotificationAction.Unknown - - notificationAction: NotificationsManager.encryptedGroupChatNotificationAction - onNotificationActionChanged: { - if (notificationAction && NotificationsManager.encryptedGroupChatNotificationAction != notificationAction) { - NotificationsManager.encryptedGroupChatNotificationAction = notificationAction - } - } - } - NotificationRuleItem { - text: i18n("Room upgrade messages") - - notificationsOn: notificationLayout.isNotificationRuleOn(NotificationsManager.tombstoneNotificationAction) - noisyOn: notificationLayout.isNotificationRuleNoisy(NotificationsManager.tombstoneNotificationAction) - highlightable: true - highlightOn: notificationLayout.isNotificationRuleHighlight(NotificationsManager.tombstoneNotificationAction) - enabled: NotificationsManager.tombstoneNotificationAction !== PushNotificationAction.Unknown - - notificationAction: NotificationsManager.tombstoneNotificationAction - onNotificationActionChanged: { - if (notificationAction && NotificationsManager.tombstoneNotificationAction != notificationAction) { - NotificationsManager.tombstoneNotificationAction = notificationAction - } - } + delegate: ruleDelegate } } } @@ -127,37 +67,17 @@ Kirigami.ScrollablePage { MobileForm.FormCardHeader { title: i18n("@Mentions") } - NotificationRuleItem { - text: i18n("Messages containing my display name") + Repeater { + model: KSortFilterProxyModel { + sourceModel: Controller.pushRuleModel - notificationsOn: notificationLayout.isNotificationRuleOn(NotificationsManager.displayNameNotificationAction) - noisyOn: notificationLayout.isNotificationRuleNoisy(NotificationsManager.displayNameNotificationAction) - highlightable: true - highlightOn: notificationLayout.isNotificationRuleHighlight(NotificationsManager.displayNameNotificationAction) - enabled: NotificationsManager.displayNameNotificationAction !== PushNotificationAction.Unknown - - notificationAction: NotificationsManager.displayNameNotificationAction - onNotificationActionChanged: { - if (notificationAction && NotificationsManager.displayNameNotificationAction != notificationAction) { - NotificationsManager.displayNameNotificationAction = notificationAction + filterRowCallback: function(source_row, source_parent) { + let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) + return sectionRole == PushNotificationSection.Mentions; } } - } - NotificationRuleItem { - text: i18n("Whole room (@room) notifications") - notificationsOn: notificationLayout.isNotificationRuleOn(NotificationsManager.roomNotificationAction) - noisyOn: notificationLayout.isNotificationRuleNoisy(NotificationsManager.roomNotificationAction) - highlightable: true - highlightOn: notificationLayout.isNotificationRuleHighlight(NotificationsManager.roomNotificationAction) - enabled: NotificationsManager.roomNotificationAction !== PushNotificationAction.Unknown - - notificationAction: NotificationsManager.roomNotificationAction - onNotificationActionChanged: { - if (notificationAction && NotificationsManager.roomNotificationAction != notificationAction) { - NotificationsManager.roomNotificationAction = notificationAction - } - } + delegate: ruleDelegate } } } @@ -171,47 +91,17 @@ Kirigami.ScrollablePage { MobileForm.FormCardHeader { title: i18n("Keywords") } - NotificationRuleItem { - id: keywordNotificationAction - text: i18n("Messages containing my keywords") - - notificationsOn: true - notificationsOnModifiable: false - noisyOn: notificationLayout.isNotificationRuleNoisy(NotificationsManager.keywordNotificationAction) - highlightable: true - highlightOn: notificationLayout.isNotificationRuleHighlight(NotificationsManager.keywordNotificationAction) - enabled: NotificationsManager.keywordNotificationAction !== PushNotificationAction.Unknown && - NotificationsManager.keywordNotificationAction !== PushNotificationAction.Off - - notificationAction: NotificationsManager.keywordNotificationAction - onNotificationActionChanged: { - if (notificationAction && NotificationsManager.keywordNotificationAction != notificationAction) { - NotificationsManager.keywordNotificationAction = notificationAction - } - } - } - MobileForm.FormDelegateSeparator {} Repeater { - model: KeywordNotificationRuleModel { - id: keywordNotificationRuleModel - } + model: KSortFilterProxyModel { + sourceModel: Controller.pushRuleModel - delegate: NotificationRuleItem { - text: name - notificationAction: keywordNotificationAction.notificationAction - notificationsOn: keywordNotificationAction.notificationsOn - notificationsOnModifiable: false - noisyOn: keywordNotificationAction.noisyOn - noisyModifiable: false - highlightOn: keywordNotificationAction.highlightOn - deletable: true - - onDeleteItemChanged: { - if (deleteItem && deletable) { - keywordNotificationRuleModel.removeKeywordAtIndex(index) - } + filterRowCallback: function(source_row, source_parent) { + let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) + return sectionRole == PushNotificationSection.Keywords; } } + + delegate: ruleDelegate } MobileForm.AbstractFormDelegate { Layout.fillWidth: true @@ -234,7 +124,7 @@ Kirigami.ScrollablePage { } onAccepted: { - keywordNotificationRuleModel.addKeyword(keywordAddField.text, PushNotificationAction.On) + Controller.pushRuleModel.addKeyword(keywordAddField.text) keywordAddField.text = "" } } @@ -248,7 +138,7 @@ Kirigami.ScrollablePage { enabled: NotificationsManager.keywordNotificationAction !== PushNotificationAction.Unknown onClicked: { - keywordNotificationRuleModel.addKeyword(keywordAddField.text, PushNotificationAction.On) + Controller.pushRuleModel.addKeyword(keywordAddField.text) keywordAddField.text = "" } @@ -271,59 +161,29 @@ Kirigami.ScrollablePage { MobileForm.FormCardHeader { title: i18n("Invites") } - NotificationRuleItem { - text: i18n("Invites to a room") + Repeater { + model: KSortFilterProxyModel { + sourceModel: Controller.pushRuleModel - notificationsOn: notificationLayout.isNotificationRuleOn(NotificationsManager.inviteNotificationAction) - noisyOn: notificationLayout.isNotificationRuleNoisy(NotificationsManager.inviteNotificationAction) - highlightable: true - highlightOn: notificationLayout.isNotificationRuleHighlight(NotificationsManager.inviteNotificationAction) - enabled: NotificationsManager.inviteNotificationAction !== PushNotificationAction.Unknown - - notificationAction: NotificationsManager.inviteNotificationAction - onNotificationActionChanged: { - if (notificationAction && NotificationsManager.inviteNotificationAction != notificationAction) { - NotificationsManager.inviteNotificationAction = notificationAction + filterRowCallback: function(source_row, source_parent) { + let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) + return sectionRole == PushNotificationSection.Invites; } } - } - NotificationRuleItem { - text: i18n("Call invitation") - // TODO enable this option when calls are supported - visible: false - - notificationsOn: notificationLayout.isNotificationRuleOn(NotificationsManager.callInviteNotificationAction) - noisyOn: notificationLayout.isNotificationRuleNoisy(NotificationsManager.callInviteNotificationAction) - highlightable: true - highlightOn: notificationLayout.isNotificationRuleHighlight(NotificationsManager.callInviteNotificationAction) - enabled: NotificationsManager.callInviteNotificationAction !== PushNotificationAction.Unknown - - notificationAction: NotificationsManager.callInviteNotificationAction - onNotificationActionChanged: { - if (notificationAction && NotificationsManager.callInviteNotificationAction != notificationAction) { - NotificationsManager.callInviteNotificationAction = notificationAction - } - } + delegate: ruleDelegate } } } + } - function isNotificationRuleOn(action) { - return action == PushNotificationAction.On || - action == PushNotificationAction.Noisy || - action == PushNotificationAction.Highlight || - action == PushNotificationAction.NoisyHighlight - } - - function isNotificationRuleNoisy(action) { - return action == PushNotificationAction.Noisy || - action == PushNotificationAction.NoisyHighlight - } - - function isNotificationRuleHighlight(action) { - return action == PushNotificationAction.Highlight || - action == PushNotificationAction.NoisyHighlight + Component { + id: ruleDelegate + NotificationRuleItem { + onDeleteRule: { + Controller.pushRuleModel.removeKeyword(id) + } + onActionChanged: (action) => Controller.pushRuleModel.setPushRuleAction(id, action) } } } diff --git a/src/qml/Settings/NotificationRuleItem.qml b/src/qml/Settings/NotificationRuleItem.qml index b8dc1ac0c..89e69e38f 100644 --- a/src/qml/Settings/NotificationRuleItem.qml +++ b/src/qml/Settings/NotificationRuleItem.qml @@ -11,19 +11,25 @@ import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm import org.kde.neochat 1.0 MobileForm.AbstractFormDelegate { - id: notificationRuleItem + id: root - property var notificationAction: PushNotificationAction.Unkown - property bool notificationsOn: false - property bool notificationsOnModifiable: true - property bool noisyOn: false - property bool noisyModifiable: true - property bool highlightOn: false - property bool highlightable: false - property bool deleteItem: false - property bool deletable: false + required property string id + required property string name + required property int ruleAction + required property bool highlightable + required property bool deletable + + readonly property bool notificationsOn: isNotificationRuleOn(ruleAction) + readonly property bool notificationsOnModifiable: !deletable + readonly property bool highlightOn: isNotificationRuleHighlight(ruleAction) + + signal actionChanged(int action) + signal deleteRule() Layout.fillWidth: true + enabled: ruleAction !== PushNotificationAction.Unknown + + text: name onClicked: { notificationAction = nextNotificationRuleAction(notificationAction) @@ -42,8 +48,8 @@ MobileForm.AbstractFormDelegate { background: Rectangle { visible: notificationsOn Kirigami.Theme.colorSet: Kirigami.Theme.Button - color: highlightOn ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor - opacity: highlightOn ? 1 : 0.3 + color: highlightOn && highlightable ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor + opacity: highlightOn && highlightable ? 1 : 0.3 radius: height / 2 } } @@ -51,7 +57,7 @@ MobileForm.AbstractFormDelegate { Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter - text: notificationRuleItem.text + text: root.text elide: Text.ElideRight wrapMode: Text.Wrap maximumLineCount: 2 @@ -67,13 +73,13 @@ MobileForm.AbstractFormDelegate { icon.name: checked ? "notifications" : "notifications-disabled" display: QQC2.AbstractButton.IconOnly - visible: notificationRuleItem.notificationsOnModifiable + visible: root.notificationsOnModifiable checkable: true - checked: notificationRuleItem.notificationsOn - enabled: notificationRuleItem.enabled + checked: root.notificationsOn + enabled: root.enabled down: checked onToggled: { - notificationRuleItem.notificationAction = notificationRuleItem.notifcationRuleAction() + root.actionChanged(root.notifcationRuleAction()) } QQC2.ToolTip { @@ -89,13 +95,12 @@ MobileForm.AbstractFormDelegate { icon.name: checked ? "audio-volume-high" : "audio-volume-muted" display: QQC2.AbstractButton.IconOnly - visible: notificationRuleItem.noisyModifiable checkable: true - checked: notificationRuleItem.noisyOn - enabled: (onButton.checked || !notificationRuleItem.notificationsOnModifiable) && notificationRuleItem.enabled + checked: isNotificationRuleNoisy(root.ruleAction) + enabled: (onButton.checked || !root.notificationsOnModifiable) && root.enabled down: checked onToggled: { - notificationRuleItem.notificationAction = notificationRuleItem.notifcationRuleAction() + root.actionChanged(root.notifcationRuleAction()) } QQC2.ToolTip { @@ -111,13 +116,13 @@ MobileForm.AbstractFormDelegate { icon.name: "draw-highlight" display: QQC2.AbstractButton.IconOnly - visible: notificationRuleItem.highlightable + visible: root.highlightable checkable: true - checked: notificationRuleItem.highlightOn - enabled: (onButton.checked || !notificationRuleItem.notificationsOnModifiable) && notificationRuleItem.enabled + checked: root.highlightOn + enabled: (onButton.checked || !root.notificationsOnModifiable) && root.enabled down: checked onToggled: { - notificationRuleItem.notificationAction = notificationRuleItem.notifcationRuleAction() + root.actionChanged(root.notifcationRuleAction()) } QQC2.ToolTip { @@ -131,10 +136,10 @@ MobileForm.AbstractFormDelegate { Accessible.name: i18n("Delete keyword") icon.name: "edit-delete-remove" - visible: notificationRuleItem.deletable + visible: root.deletable onClicked: { - notificationRuleItem.deleteItem = !notificationRuleItem.deleteItem + root.deleteRule() } } } @@ -142,11 +147,11 @@ MobileForm.AbstractFormDelegate { function notifcationRuleAction() { if (onButton.checked) { - if (noisyButton.checked && highlightButton.checked) { + if (noisyButton.checked && highlightButton.checked && root.highlightable) { return PushNotificationAction.NoisyHighlight } else if (noisyButton.checked) { return PushNotificationAction.Noisy - } else if (highlightButton.checked) { + } else if (highlightButton.checked && root.highlightable) { return PushNotificationAction.Highlight } else { return PushNotificationAction.On @@ -166,17 +171,34 @@ MobileForm.AbstractFormDelegate { } while (!finished) { - if (action == PushNotificationAction.Off && !notificationRuleItem.notificationsOnModifiable) { + if (action == PushNotificationAction.Off && !root.notificationsOnModifiable) { action = PushNotificationAction.On - } else if (action == PushNotificationAction.Noisy && !notificationRuleItem.noisyModifiable) { + } else if (action == PushNotificationAction.Noisy) { action = PushNotificationAction.Highlight - } else if (action == PushNotificationAction.Highlight && !notificationRuleItem.highlightable) { + } else if (action == PushNotificationAction.Highlight && !root.highlightable) { action = PushNotificationAction.Off } else { finished = true } } - return action + actionChanged(action) + } + + function isNotificationRuleOn(action) { + return action == PushNotificationAction.On || + action == PushNotificationAction.Noisy || + action == PushNotificationAction.Highlight || + action == PushNotificationAction.NoisyHighlight + } + + function isNotificationRuleNoisy(action) { + return action == PushNotificationAction.Noisy || + action == PushNotificationAction.NoisyHighlight + } + + function isNotificationRuleHighlight(action) { + return action == PushNotificationAction.Highlight || + action == PushNotificationAction.NoisyHighlight } }