Move more stuff to settings

This commit is contained in:
James Graham
2025-04-12 20:14:59 +01:00
parent 32ee590cef
commit 3a4bc18d45
22 changed files with 50 additions and 45 deletions

View File

@@ -0,0 +1,222 @@
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "accountemoticonmodel.h"
#include <QImage>
#include <QMimeDatabase>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/events/eventcontent.h>
#include <qcoro/qcorosignal.h>
#include "neochatconnection.h"
using namespace Quotient;
AccountEmoticonModel::AccountEmoticonModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int AccountEmoticonModel::rowCount(const QModelIndex &index) const
{
Q_UNUSED(index);
if (!m_images) {
return 0;
}
return m_images->images.size();
}
QVariant AccountEmoticonModel::data(const QModelIndex &index, int role) const
{
if (m_connection == nullptr) {
return {};
}
const auto &row = index.row();
const auto &image = m_images->images[row];
if (role == UrlRole) {
return m_connection->makeMediaUrl(image.url).toString();
}
if (role == BodyRole) {
if (image.body) {
return *image.body;
}
}
if (role == ShortCodeRole) {
return image.shortcode;
}
if (role == IsStickerRole) {
if (image.usage) {
return image.usage->isEmpty() || image.usage->contains("sticker"_L1);
}
if (m_images->pack && m_images->pack->usage) {
return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("sticker"_L1);
}
return true;
}
if (role == IsEmojiRole) {
if (image.usage) {
return image.usage->isEmpty() || image.usage->contains("emoticon"_L1);
}
if (m_images->pack && m_images->pack->usage) {
return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("emoticon"_L1);
}
return true;
}
return {};
}
QHash<int, QByteArray> AccountEmoticonModel::roleNames() const
{
return {
{AccountEmoticonModel::UrlRole, "url"},
{AccountEmoticonModel::BodyRole, "body"},
{AccountEmoticonModel::ShortCodeRole, "shortcode"},
{AccountEmoticonModel::IsStickerRole, "isSticker"},
{AccountEmoticonModel::IsEmojiRole, "isEmoji"},
};
}
NeoChatConnection *AccountEmoticonModel::connection() const
{
return m_connection;
}
void AccountEmoticonModel::setConnection(NeoChatConnection *connection)
{
if (m_connection) {
disconnect(m_connection, nullptr, this, nullptr);
}
m_connection = connection;
Q_EMIT connectionChanged();
connect(m_connection, &Connection::accountDataChanged, this, [this](QString type) {
if (type == u"im.ponies.user_emotes"_s) {
reloadEmoticons();
}
});
reloadEmoticons();
}
void AccountEmoticonModel::reloadEmoticons()
{
if (m_connection == nullptr) {
return;
}
QJsonObject json;
if (m_connection->hasAccountData("im.ponies.user_emotes"_L1)) {
json = m_connection->accountData("im.ponies.user_emotes"_L1)->contentJson();
}
const auto &content = ImagePackEventContent(json);
beginResetModel();
m_images = content;
endResetModel();
}
void AccountEmoticonModel::deleteEmoticon(int index)
{
if (m_connection == nullptr) {
return;
}
QJsonObject data;
m_images->images.removeAt(index);
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_L1, data);
}
void AccountEmoticonModel::setEmoticonBody(int index, const QString &text)
{
if (m_connection == nullptr) {
return;
}
m_images->images[index].body = text;
QJsonObject data;
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_L1, data);
}
void AccountEmoticonModel::setEmoticonShortcode(int index, const QString &shortcode)
{
if (m_connection == nullptr) {
return;
}
m_images->images[index].shortcode = shortcode;
QJsonObject data;
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_L1, data);
}
void AccountEmoticonModel::setEmoticonImage(int index, const QUrl &source)
{
if (m_connection == nullptr) {
return;
}
doSetEmoticonImage(index, source);
}
QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl source)
{
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
co_await qCoro(job.get(), &BaseJob::finished);
if (job->error() != BaseJob::NoError) {
co_return;
}
m_images->images[index].url = job->contentUri();
auto mime = QMimeDatabase().mimeTypeForUrl(source);
source.setScheme("file"_L1);
QFileInfo fileInfo(source.isLocalFile() ? source.toLocalFile() : source.toString());
EventContent::ImageInfo info;
if (mime.name().startsWith("image/"_L1)) {
QImage image(source.toLocalFile());
info = EventContent::ImageInfo(source, fileInfo.size(), mime, image.size(), fileInfo.fileName());
}
m_images->images[index].info = info;
QJsonObject data;
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_L1, data);
}
QCoro::Task<void> AccountEmoticonModel::doAddEmoticon(QUrl source, QString shortcode, QString description, QString type)
{
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
co_await qCoro(job.get(), &BaseJob::finished);
if (job->error() != BaseJob::NoError) {
co_return;
}
auto mime = QMimeDatabase().mimeTypeForUrl(source);
source.setScheme("file"_L1);
QFileInfo fileInfo(source.isLocalFile() ? source.toLocalFile() : source.toString());
EventContent::ImageInfo info;
if (mime.name().startsWith("image/"_L1)) {
QImage image(source.toLocalFile());
info = EventContent::ImageInfo(source, fileInfo.size(), mime, image.size(), fileInfo.fileName());
}
m_images->images.append(ImagePackEventContent::ImagePackImage{
shortcode,
job->contentUri(),
description,
info,
QStringList{type},
});
QJsonObject data;
m_images->fillJson(&data);
m_connection->setAccountData("im.ponies.user_emotes"_L1, data);
}
void AccountEmoticonModel::addEmoticon(const QUrl &source, const QString &shortcode, const QString &description, const QString &type)
{
if (m_connection == nullptr) {
return;
}
doAddEmoticon(source, shortcode, description, type);
}
#include "moc_accountemoticonmodel.cpp"

View File

@@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "events/imagepackevent.h"
#include <QAbstractListModel>
#include <QCoroTask>
#include <QList>
#include <QObject>
#include <QPointer>
#include <QQmlEngine>
class NeoChatConnection;
/**
* @class AccountEmoticonModel
*
* This class defines the model for visualising the account stickers and emojis.
*
* This is based upon the im.ponies.user_emotes spec (MSC2545).
*/
class AccountEmoticonModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The connection to get emoticons from.
*/
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
enum Roles {
UrlRole = Qt::UserRole + 1, /**< The URL for the emoticon. */
ShortCodeRole, /**< The shortcode for the emoticon. */
BodyRole, //**< A textual description of the emoticon */
IsStickerRole, //**< Whether this emoticon is a sticker */
IsEmojiRole, //**< Whether this emoticon is an emoji */
};
explicit AccountEmoticonModel(QObject *parent = nullptr);
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &index) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] NeoChatConnection *connection() const;
void setConnection(NeoChatConnection *connection);
/**
* @brief Deletes the emoticon at the given index.
*/
Q_INVOKABLE void deleteEmoticon(int index);
/**
* @brief Changes the description for the emoticon at the given index.
*/
Q_INVOKABLE void setEmoticonBody(int index, const QString &text);
/**
* @brief Changes the shortcode for the emoticon at the given index.
*/
Q_INVOKABLE void setEmoticonShortcode(int index, const QString &shortCode);
/**
* @brief Changes the image for the emoticon at the given index.
*/
Q_INVOKABLE void setEmoticonImage(int index, const QUrl &source);
/**
* @brief Add an emoticon with the given parameters.
*/
Q_INVOKABLE void addEmoticon(const QUrl &source, const QString &shortcode, const QString &description, const QString &type);
Q_SIGNALS:
void connectionChanged();
private:
std::optional<Quotient::ImagePackEventContent> m_images;
QPointer<NeoChatConnection> m_connection;
QCoro::Task<void> doSetEmoticonImage(int index, QUrl source);
QCoro::Task<void> doAddEmoticon(QUrl source, QString shortcode, QString description, QString type);
void reloadEmoticons();
};

View File

@@ -0,0 +1,179 @@
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "devicesmodel.h"
#include <QDateTime>
#include <QLocale>
#include <KLocalizedString>
#include <Quotient/csapi/device_management.h>
#include <Quotient/user.h>
#include "neochatconnection.h"
using namespace Quotient;
DevicesModel::DevicesModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(m_connection, &Connection::sessionVerified, this, [this](const QString &, const QString &deviceId) {
const auto it = std::find_if(m_devices.begin(), m_devices.end(), [deviceId](const Quotient::Device &device) {
return device.deviceId == deviceId;
});
if (it != m_devices.end()) {
const auto index = this->index(it - m_devices.begin());
Q_EMIT dataChanged(index, index, {Type});
}
});
}
void DevicesModel::fetchDevices()
{
if (m_connection) {
auto job = m_connection->callApi<GetDevicesJob>();
connect(job, &BaseJob::success, this, [this, job]() {
beginResetModel();
m_devices = job->devices();
endResetModel();
Q_EMIT countChanged();
});
}
}
QVariant DevicesModel::data(const QModelIndex &index, int role) const
{
if (index.row() < 0 || index.row() >= rowCount(QModelIndex())) {
return {};
}
const auto &device = m_devices[index.row()];
switch (role) {
case Id:
return device.deviceId;
case DisplayName:
return device.displayName;
case LastIp:
return device.lastSeenIp;
case LastTimestamp:
if (device.lastSeenTs) {
return *device.lastSeenTs;
} else {
return false;
}
case TimestampString:
if (device.lastSeenTs) {
return QDateTime::fromMSecsSinceEpoch(*device.lastSeenTs).toString(QLocale().dateTimeFormat(QLocale::ShortFormat));
} else {
return false;
}
case Type:
if (device.deviceId == m_connection->deviceId()) {
return This;
}
if (!m_connection->isKnownE2eeCapableDevice(m_connection->userId(), device.deviceId)) {
return Unencrypted;
}
if (m_connection->isVerifiedDevice(m_connection->userId(), device.deviceId)) {
return Verified;
} else {
return Unverified;
}
}
return {};
}
int DevicesModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_devices.size();
}
QHash<int, QByteArray> DevicesModel::roleNames() const
{
return {
{Id, "id"},
{DisplayName, "displayName"},
{LastIp, "lastIp"},
{LastTimestamp, "lastTimestamp"},
{TimestampString, "timestamp"},
{Type, "type"},
};
}
void DevicesModel::logout(const QString &deviceId, const QString &password)
{
int index;
for (index = 0; m_devices[index].deviceId != deviceId; index++)
;
auto job = m_connection->callApi<DeleteDeviceJob>(m_devices[index].deviceId);
connect(job, &BaseJob::result, this, [this, job, password, index] {
auto onSuccess = [this, index]() {
beginRemoveRows(QModelIndex(), index, index);
m_devices.remove(index);
endRemoveRows();
Q_EMIT countChanged();
};
if (job->error() != BaseJob::Success) {
QJsonObject replyData = job->jsonData();
AuthenticationData authData;
authData.session = replyData["session"_L1].toString();
authData.authInfo["password"_L1] = password;
authData.type = "m.login.password"_L1;
authData.authInfo["identifier"_L1] = QJsonObject{{"type"_L1, "m.id.user"_L1}, {"user"_L1, m_connection->user()->id()}};
auto innerJob = m_connection->callApi<DeleteDeviceJob>(m_devices[index].deviceId, authData);
connect(innerJob.get(), &BaseJob::success, this, onSuccess);
} else {
onSuccess();
}
});
}
void DevicesModel::setName(const QString &deviceId, const QString &name)
{
int index;
for (index = 0; m_devices[index].deviceId != deviceId; index++)
;
auto job = m_connection->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name);
QString oldName = m_devices[index].displayName;
beginResetModel();
m_devices[index].displayName = name;
endResetModel();
connect(job, &BaseJob::failure, this, [this, index, oldName]() {
beginResetModel();
m_devices[index].displayName = oldName;
endResetModel();
});
}
NeoChatConnection *DevicesModel::connection() const
{
return m_connection;
}
void DevicesModel::setConnection(NeoChatConnection *connection)
{
if (m_connection) {
disconnect(m_connection, nullptr, this, nullptr);
}
m_connection = connection;
Q_EMIT connectionChanged();
fetchDevices();
connect(m_connection, &Connection::sessionVerified, this, [this](const QString &userId, const QString &deviceId) {
Q_UNUSED(deviceId);
if (userId == m_connection->userId()) {
fetchDevices();
}
});
connect(m_connection, &Connection::finishedQueryingKeys, this, [this]() {
fetchDevices();
});
}
#include "moc_devicesmodel.cpp"

View File

@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <Quotient/csapi/definitions/client_device.h>
class NeoChatConnection;
/**
* @class DevicesModel
*
* This class defines the model for managing the devices of the local user.
*
* A device is any session where the local user is logged into a client. This means
* the same physical device can have multiple sessions for example if the user uses
* multiple clients on the same machine.
*/
class DevicesModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current connection that the model is getting its devices from.
*/
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged REQUIRED)
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
Id, /**< The device ID. */
DisplayName, /**< Display name set by the user for this device. */
LastIp, /**< The IP address where this device was last seen. */
LastTimestamp, /**< The timestamp when this devices was last seen. */
TimestampString, /**< String for the timestamp when this devices was last seen. */
Type, /**< The category to sort this device into. */
};
Q_ENUM(Roles)
enum DeviceType {
This,
Verified,
Unverified,
Unencrypted,
};
Q_ENUM(DeviceType);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Logout the device with the given id.
*/
Q_INVOKABLE void logout(const QString &deviceId, const QString &password);
/**
* @brief Set the display name of the device with the given id.
*/
Q_INVOKABLE void setName(const QString &deviceId, const QString &name);
explicit DevicesModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatConnection *connection() const;
void setConnection(NeoChatConnection *connection);
Q_SIGNALS:
void connectionChanged();
void countChanged();
private:
void fetchDevices();
QList<Quotient::Device> m_devices;
QPointer<NeoChatConnection> m_connection;
};

View File

@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "devicesproxymodel.h"
#include "devicesmodel.h"
int DevicesProxyModel::type() const
{
return m_type;
}
void DevicesProxyModel::setType(int type)
{
m_type = type;
Q_EMIT typeChanged();
}
bool DevicesProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent)
return sourceModel()->data(sourceModel()->index(source_row, 0), DevicesModel::Type).toInt() == m_type;
}
DevicesProxyModel::DevicesProxyModel(QObject *parent)
: QSortFilterProxyModel(parent)
, m_type(0)
{
setSortRole(DevicesModel::LastTimestamp);
sort(0, Qt::DescendingOrder);
}
#include "moc_devicesproxymodel.cpp"

View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
class DevicesProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int type READ type WRITE setType NOTIFY typeChanged)
public:
DevicesProxyModel(QObject *parent = nullptr);
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
void setType(int type);
[[nodiscard]] int type() const;
Q_SIGNALS:
void typeChanged();
private:
int m_type;
};

View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "emoticonfiltermodel.h"
#include "accountemoticonmodel.h"
#include "models/stickermodel.h"
EmoticonFilterModel::EmoticonFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
connect(this, &EmoticonFilterModel::sourceModelChanged, this, [this]() {
if (dynamic_cast<StickerModel *>(sourceModel())) {
m_stickerRole = StickerModel::IsStickerRole;
m_emojiRole = StickerModel::IsEmojiRole;
} else {
m_stickerRole = AccountEmoticonModel::IsStickerRole;
m_emojiRole = AccountEmoticonModel::IsEmojiRole;
}
});
}
bool EmoticonFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
Q_UNUSED(sourceParent);
auto stickerUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), m_stickerRole).toBool();
auto emojiUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), m_emojiRole).toBool();
return (stickerUsage && m_showStickers) || (emojiUsage && m_showEmojis);
}
bool EmoticonFilterModel::showStickers() const
{
return m_showStickers;
}
void EmoticonFilterModel::setShowStickers(bool showStickers)
{
beginResetModel();
m_showStickers = showStickers;
endResetModel();
Q_EMIT showStickersChanged();
}
bool EmoticonFilterModel::showEmojis() const
{
return m_showEmojis;
}
void EmoticonFilterModel::setShowEmojis(bool showEmojis)
{
beginResetModel();
m_showEmojis = showEmojis;
endResetModel();
Q_EMIT showEmojisChanged();
}
#include "moc_emoticonfiltermodel.cpp"

View File

@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class EmoticonFilterModel
*
* This class creates a custom QSortFilterProxyModel for filtering a emoticon by type
* (Sticker or Emoji).
*/
class EmoticonFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief Whether stickers should be shown
*/
Q_PROPERTY(bool showStickers READ showStickers WRITE setShowStickers NOTIFY showStickersChanged)
/**
* @brief Whether emojis show be shown
*/
Q_PROPERTY(bool showEmojis READ showEmojis WRITE setShowEmojis NOTIFY showEmojisChanged)
public:
explicit EmoticonFilterModel(QObject *parent = nullptr);
/**
* @brief Custom filter function checking the type of emoticon
*
* @note The filter cannot be modified and will always use the same filter properties.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
[[nodiscard]] bool showStickers() const;
void setShowStickers(bool showStickers);
[[nodiscard]] bool showEmojis() const;
void setShowEmojis(bool showEmojis);
Q_SIGNALS:
void showStickersChanged();
void showEmojisChanged();
private:
bool m_showStickers = false;
bool m_showEmojis = false;
int m_stickerRole = 0;
int m_emojiRole = 0;
};

View File

@@ -0,0 +1,279 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "permissionsmodel.h"
#include <Quotient/events/roompowerlevelsevent.h>
#include <KLazyLocalizedString>
#include "enums/powerlevel.h"
using namespace Qt::Literals::StringLiterals;
namespace
{
constexpr auto UsersDefaultKey = "users_default"_L1;
constexpr auto StateDefaultKey = "state_default"_L1;
constexpr auto EventsDefaultKey = "events_default"_L1;
constexpr auto InviteKey = "invite"_L1;
constexpr auto KickKey = "kick"_L1;
constexpr auto BanKey = "ban"_L1;
constexpr auto RedactKey = "redact"_L1;
static const QStringList defaultPermissions = {
UsersDefaultKey,
StateDefaultKey,
EventsDefaultKey,
};
static const QStringList basicPermissions = {
InviteKey,
KickKey,
BanKey,
RedactKey,
};
static const QStringList knownPermissions = {
u"m.reaction"_s,
u"m.room.redaction"_s,
u"m.room.power_levels"_s,
u"m.room.name"_s,
u"m.room.avatar"_s,
u"m.room.canonical_alias"_s,
u"m.room.topic"_s,
u"m.room.encryption"_s,
u"m.room.history_visibility"_s,
u"m.room.pinned_events"_s,
u"m.room.tombstone"_s,
u"m.room.server_acl"_s,
u"m.space.child"_s,
u"m.space.parent"_s,
};
// Alternate name text for default permissions.
static const QHash<QString, KLazyLocalizedString> permissionNames = {
{UsersDefaultKey, kli18nc("Room permission type", "Default user power level")},
{StateDefaultKey, kli18nc("Room permission type", "Default power level to set the room state")},
{EventsDefaultKey, kli18nc("Room permission type", "Default power level to send messages")},
{InviteKey, kli18nc("Room permission type", "Invite users")},
{KickKey, kli18nc("Room permission type", "Kick users")},
{BanKey, kli18nc("Room permission type", "Ban users")},
{RedactKey, kli18nc("Room permission type", "Remove messages sent by other users")},
{u"m.reaction"_s, kli18nc("Room permission type", "Send reactions")},
{u"m.room.redaction"_s, kli18nc("Room permission type", "Remove their own messages")},
{u"m.room.power_levels"_s, kli18nc("Room permission type", "Change user permissions")},
{u"m.room.name"_s, kli18nc("Room permission type", "Change the room name")},
{u"m.room.avatar"_s, kli18nc("Room permission type", "Change the room avatar")},
{u"m.room.canonical_alias"_s, kli18nc("Room permission type", "Change the room canonical alias")},
{u"m.room.topic"_s, kli18nc("Room permission type", "Change the room topic")},
{u"m.room.encryption"_s, kli18nc("Room permission type", "Enable encryption for the room")},
{u"m.room.history_visibility"_s, kli18nc("Room permission type", "Change the room history visibility")},
{u"m.room.pinned_events"_s, kli18nc("Room permission type", "Set pinned events")},
{u"m.room.tombstone"_s, kli18nc("Room permission type", "Upgrade the room")},
{u"m.room.server_acl"_s, kli18nc("Room permission type", "Set the room server access control list (ACL)")},
{u"m.space.child"_s, kli18nc("Room permission type", "Set the children of this space")},
{u"m.space.parent"_s, kli18nc("Room permission type", "Set the parent space of this room")},
};
// Subtitles for the default values.
static const QHash<QString, KLazyLocalizedString> permissionSubtitles = {
{UsersDefaultKey, kli18nc("Room permission type", "This is the power level for all new users when joining the room")},
{StateDefaultKey, kli18nc("Room permission type", "This is used for all state events that do not have their own entry here")},
{EventsDefaultKey, kli18nc("Room permission type", "This is used for all message events that do not have their own entry here")},
};
// Permissions that should use the event default.
static const QStringList eventPermissions = {
u"m.room.message"_s,
u"m.reaction"_s,
u"m.room.redaction"_s,
};
};
PermissionsModel::PermissionsModel(QObject *parent)
: QAbstractListModel(parent)
{
}
NeoChatRoom *PermissionsModel::room() const
{
return m_room;
}
void PermissionsModel::setRoom(NeoChatRoom *room)
{
if (room == m_room) {
return;
}
m_room = room;
Q_EMIT roomChanged();
initializeModel();
}
void PermissionsModel::initializeModel()
{
beginResetModel();
m_permissions.clear();
if (m_room == nullptr) {
endResetModel();
return;
}
const auto currentPowerLevelEvent = m_room->currentState().get<Quotient::RoomPowerLevelsEvent>();
if (currentPowerLevelEvent == nullptr) {
return;
}
m_permissions.append(defaultPermissions);
m_permissions.append(basicPermissions);
m_permissions.append(knownPermissions);
for (const auto &event : currentPowerLevelEvent->events().keys()) {
if (!m_permissions.contains(event)) {
m_permissions += event;
}
}
endResetModel();
}
QVariant PermissionsModel::data(const QModelIndex &index, int role) const
{
if (m_room == nullptr || !index.isValid()) {
return {};
}
if (index.row() >= rowCount()) {
qDebug() << "PushRuleModel, something's wrong: index.row() >= m_rules.count()";
return {};
}
const auto permission = m_permissions.value(index.row());
if (role == NameRole) {
if (permissionNames.keys().contains(permission)) {
return permissionNames.value(permission).toString();
}
return permission;
}
if (role == SubtitleRole) {
if (knownPermissions.contains(permission) && permissionNames.keys().contains(permission)) {
return permission;
}
if (permissionSubtitles.contains(permission)) {
return permissionSubtitles.value(permission).toString();
}
return QString();
}
if (role == TypeRole) {
return permission;
}
if (role == LevelRole) {
const auto level = powerLevel(permission);
if (level.has_value()) {
return *level;
}
return {};
}
if (role == LevelNameRole) {
const auto level = powerLevel(permission);
if (level.has_value()) {
return i18nc("%1 is the name of the power level, e.g. admin and %2 is the value that represents.",
"%1 (%2)",
PowerLevel::nameForLevel(PowerLevel::levelForValue(*level)),
*level);
}
return QString();
}
if (role == IsDefaultValueRole) {
return defaultPermissions.contains(permission);
}
if (role == IsBasicPermissionRole) {
return basicPermissions.contains(permission);
}
return {};
}
int PermissionsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_permissions.count();
}
QHash<int, QByteArray> PermissionsModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[NameRole] = "name";
roles[SubtitleRole] = "subtitle";
roles[TypeRole] = "type";
roles[LevelRole] = "level";
roles[LevelNameRole] = "levelName";
roles[IsDefaultValueRole] = "isDefaultValue";
roles[IsBasicPermissionRole] = "isBasicPermission";
return roles;
}
std::optional<int> PermissionsModel::powerLevel(const QString &permission) const
{
if (m_room == nullptr) {
return std::nullopt;
}
if (const auto currentPowerLevelEvent = m_room->currentState().get<Quotient::RoomPowerLevelsEvent>()) {
if (permission == BanKey) {
return currentPowerLevelEvent->ban();
} else if (permission == KickKey) {
return currentPowerLevelEvent->kick();
} else if (permission == InviteKey) {
return currentPowerLevelEvent->invite();
} else if (permission == RedactKey) {
return currentPowerLevelEvent->redact();
} else if (permission == UsersDefaultKey) {
return currentPowerLevelEvent->usersDefault();
} else if (permission == StateDefaultKey) {
return currentPowerLevelEvent->stateDefault();
} else if (permission == EventsDefaultKey) {
return currentPowerLevelEvent->eventsDefault();
} else if (eventPermissions.contains(permission)) {
return currentPowerLevelEvent->powerLevelForEvent(permission);
} else {
return currentPowerLevelEvent->powerLevelForState(permission);
}
}
return std::nullopt;
}
void PermissionsModel::setPowerLevel(const QString &permission, const int &newPowerLevel)
{
if (m_room == nullptr) {
return;
}
int clampPowerLevel = std::clamp(newPowerLevel, -1, 100);
const auto currentPowerLevel = powerLevel(permission);
if (!currentPowerLevel.has_value() || currentPowerLevel == clampPowerLevel) {
return;
}
if (auto currentPowerLevelEvent = m_room->currentState().get<Quotient::RoomPowerLevelsEvent>()) {
auto powerLevelContent = currentPowerLevelEvent->contentJson();
if (powerLevelContent.contains(permission)) {
powerLevelContent[permission] = clampPowerLevel;
// Deal with the case where a default or basic permission is missing from the event content erroneously.
} else if (defaultPermissions.contains(permission) || basicPermissions.contains(permission)) {
powerLevelContent[permission] = clampPowerLevel;
} else {
auto eventPowerLevels = powerLevelContent["events"_L1].toObject();
eventPowerLevels[permission] = clampPowerLevel;
powerLevelContent["events"_L1] = eventPowerLevels;
}
m_room->setState<Quotient::RoomPowerLevelsEvent>(Quotient::fromJson<Quotient::PowerLevelsEventContent>(powerLevelContent));
}
}
#include "moc_permissionsmodel.cpp"

View File

@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <optional>
#include "neochatroom.h"
/**
* @class PermissionsModel
*
* This class defines the model for managing room permission levels.
*/
class PermissionsModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The room to show the permissions for
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
NameRole = Qt::DisplayRole, /**< The permission name. */
SubtitleRole, /**< The description of the permission. */
TypeRole, /**< The base type of the permission, normally the event type id except for ban, kick, etc. */
LevelRole, /**< The current power level for the permission. */
LevelNameRole, /**< The current power level for the permission as a string. */
IsDefaultValueRole, /**< Whether the permission is a default value, e.g. for users. */
IsBasicPermissionRole, /**< Whether the permission is one of the basic ones, e.g. kick, ban, etc. */
};
Q_ENUM(Roles)
explicit PermissionsModel(QObject *parent = nullptr);
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
/**
* @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 Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Return the power level required for the given permission.
*/
std::optional<int> powerLevel(const QString &permission) const;
/**
* @brief Set the power level required for the given permission.
*/
Q_INVOKABLE void setPowerLevel(const QString &permission, const int &newPowerLevel);
Q_SIGNALS:
void roomChanged();
private:
QPointer<NeoChatRoom> m_room;
QStringList m_permissions;
void initializeModel();
};

View File

@@ -0,0 +1,448 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "pushrulemodel.h"
#include <QDebug>
#include <Quotient/converters.h>
#include <Quotient/csapi/definitions/push_ruleset.h>
#include <Quotient/csapi/pushrules.h>
#include <Quotient/jobs/basejob.h>
#include "enums/pushrule.h"
#include <KLazyLocalizedString>
// Alternate name text for default rules.
static const QHash<QString, KLazyLocalizedString> defaultRuleNames = {
{u".m.rule.master"_s, kli18nc("Notification type", "Enable notifications for this account")},
{u".m.rule.room_one_to_one"_s, kli18nc("Notification type", "Messages in one-to-one chats")},
{u".m.rule.encrypted_room_one_to_one"_s, kli18nc("Notification type", "Encrypted messages in one-to-one chats")},
{u".m.rule.message"_s, kli18nc("Notification type", "Messages in group chats")},
{u".m.rule.encrypted"_s, kli18nc("Notification type", "Messages in encrypted group chats")},
{u".m.rule.tombstone"_s, kli18nc("Notification type", "Room upgrade messages")},
{u".m.rule.contains_display_name"_s, kli18nc("Notification type", "Messages containing my display name")},
{u".m.rule.is_user_mention"_s, kli18nc("Notification type", "Messages which mention my Matrix user ID")},
{u".m.rule.is_room_mention"_s, kli18nc("Notification type", "Messages which mention a room")},
{u".m.rule.contains_user_name"_s, kli18nc("Notification type", "Messages containing the local part of my Matrix ID")},
{u".m.rule.roomnotif"_s, kli18nc("Notification type", "Whole room (@room) notifications")},
{u".m.rule.invite_for_me"_s, kli18nc("Notification type", "Invites to a room")},
{u".m.rule.call"_s, kli18nc("Notification type", "Call invitation")},
};
// Sections for default rules.
static const QHash<QString, PushRuleSection::Section> defaultSections = {
{u".m.rule.master"_s, PushRuleSection::Master},
{u".m.rule.room_one_to_one"_s, PushRuleSection::Room},
{u".m.rule.encrypted_room_one_to_one"_s, PushRuleSection::Room},
{u".m.rule.message"_s, PushRuleSection::Room},
{u".m.rule.encrypted"_s, PushRuleSection::Room},
{u".m.rule.tombstone"_s, PushRuleSection::Room},
{u".m.rule.contains_display_name"_s, PushRuleSection::Mentions},
{u".m.rule.is_user_mention"_s, PushRuleSection::Mentions},
{u".m.rule.is_room_mention"_s, PushRuleSection::Mentions},
{u".m.rule.contains_user_name"_s, PushRuleSection::Mentions},
{u".m.rule.roomnotif"_s, PushRuleSection::Mentions},
{u".m.rule.invite_for_me"_s, PushRuleSection::Invites},
{u".m.rule.call"_s, PushRuleSection::Undefined}, // TODO: make invites when VOIP added.
{u".m.rule.suppress_notices"_s, PushRuleSection::Undefined},
{u".m.rule.member_event"_s, PushRuleSection::Undefined},
{u".m.rule.reaction"_s, PushRuleSection::Undefined},
{u".m.rule.room.server_acl"_s, PushRuleSection::Undefined},
{u".im.vector.jitsi"_s, PushRuleSection::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 = {
u".m.rule.room_one_to_one"_s,
u".m.rule.encrypted_room_one_to_one"_s,
u".m.rule.message"_s,
u".m.rule.encrypted"_s,
};
PushRuleModel::PushRuleModel(QObject *parent)
: QAbstractListModel(parent)
{
}
void PushRuleModel::updateNotificationRules(const QString &type)
{
if (type != u"m.push_rules"_s) {
return;
}
const QJsonObject ruleDataJson = m_connection->accountDataJson(u"m.push_rules"_s);
const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson["global"_L1].toObject());
beginResetModel();
m_rules.clear();
// Doing this 5 times because PushRuleset is a struct.
setRules(ruleData.override, PushRuleKind::Override);
setRules(ruleData.content, PushRuleKind::Content);
setRules(ruleData.room, PushRuleKind::Room);
setRules(ruleData.sender, PushRuleKind::Sender);
setRules(ruleData.underride, PushRuleKind::Underride);
Q_EMIT globalNotificationsEnabledChanged();
Q_EMIT globalNotificationsSetChanged();
endResetModel();
}
void PushRuleModel::setRules(QList<Quotient::PushRule> rules, PushRuleKind::Kind kind)
{
for (const auto &rule : rules) {
QString roomId;
if (rule.conditions.size() > 0) {
for (const auto &condition : std::as_const(rule.conditions)) {
if (condition.key == u"room_id"_s) {
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;
}
PushRuleSection::Section PushRuleModel::getSection(Quotient::PushRule rule)
{
auto ruleId = rule.ruleId;
if (defaultSections.contains(ruleId)) {
return defaultSections.value(ruleId);
} else {
if (rule.ruleId.startsWith(u'.')) {
return PushRuleSection::Unknown;
}
/**
* 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.
*/
if (m_connection->room(ruleId) != nullptr) {
return PushRuleSection::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 (testUserId.startsWith(u'@') && !Quotient::serverPart(testUserId).isEmpty() && m_connection->user(testUserId) != nullptr) {
return PushRuleSection::Undefined;
}
// If the rule has push conditions and one is a room ID it is a room only keyword.
if (!rule.conditions.isEmpty()) {
for (const auto &condition : std::as_const(rule.conditions)) {
if (condition.key == u"room_id"_s) {
return PushRuleSection::RoomKeywords;
}
}
}
return PushRuleSection::Keywords;
}
}
bool PushRuleModel::globalNotificationsEnabled() const
{
auto masterIndex = getRuleIndex(u".m.rule.master"_s);
if (masterIndex > -1) {
return !m_rules[masterIndex].enabled;
}
return false;
}
void PushRuleModel::setGlobalNotificationsEnabled(bool enabled)
{
setNotificationRuleEnabled(u"override"_s, u".m.rule.master"_s, !enabled);
}
bool PushRuleModel::globalNotificationsSet() const
{
return getRuleIndex(u".m.rule.master"_s) > -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).toString();
} 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(u"."_s);
}
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<int, QByteArray> PushRuleModel::roleNames() const
{
QHash<int, QByteArray> 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, PushRuleAction::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 == PushRuleAction::Off && rule.kind == PushRuleKind::Override) {
setNotificationRuleEnabled(PushRuleKind::kindString(rule.kind), rule.id, false);
} else if (rule.kind == PushRuleKind::Override) {
setNotificationRuleEnabled(PushRuleKind::kindString(rule.kind), rule.id, true);
}
setNotificationRuleActions(PushRuleKind::kindString(rule.kind), rule.id, action);
}
void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
{
if (!m_connection) {
return;
}
PushRuleKind::Kind kind = PushRuleKind::Content;
const QList<QVariant> actions = actionToVariant(m_connection->keywordPushRuleDefault());
QList<Quotient::PushCondition> pushConditions;
if (!roomId.isEmpty()) {
kind = PushRuleKind::Override;
Quotient::PushCondition roomCondition;
roomCondition.kind = u"event_match"_s;
roomCondition.key = u"room_id"_s;
roomCondition.pattern = roomId;
pushConditions.append(roomCondition);
Quotient::PushCondition keywordCondition;
keywordCondition.kind = u"event_match"_s;
keywordCondition.key = u"content.body"_s;
keywordCondition.pattern = keyword;
pushConditions.append(keywordCondition);
}
auto job = m_connection->callApi<Quotient::SetPushRuleJob>(PushRuleKind::kindString(kind),
keyword,
actions,
QString(),
QString(),
pushConditions,
roomId.isEmpty() ? keyword : QString());
connect(job, &Quotient::BaseJob::failure, this, [job, keyword]() {
qWarning() << "Unable to set push rule for keyword %1: "_L1.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 = PushRuleKind::kindString(m_rules[index].kind);
auto job = m_connection->callApi<Quotient::DeletePushRuleJob>(kind, m_rules[index].id);
connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() {
qWarning() << "Unable to remove push rule for keyword %1: "_L1.arg(m_rules[index].id) << job->errorString();
});
}
void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled)
{
auto job = m_connection->callApi<Quotient::IsPushRuleEnabledJob>(kind, ruleId);
connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled, this]() {
if (job->enabled() != enabled) {
m_connection->callApi<Quotient::SetPushRuleEnabledJob>(kind, ruleId, enabled);
}
});
}
void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushRuleAction::Action action)
{
QList<QVariant> actions;
if (ruleId == u".m.rule.call"_s) {
actions = actionToVariant(action, u"ring"_s);
} else {
actions = actionToVariant(action);
}
m_connection->callApi<Quotient::SetPushRuleActionsJob>(kind, ruleId, actions);
}
PushRuleAction::Action PushRuleModel::variantToAction(const QList<QVariant> &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 == u"notify"_s) {
notify = true;
}
continue;
}
QJsonObject action = i.toJsonObject();
if (action["set_tweak"_L1].toString() == u"sound"_s) {
isNoisy = true;
} else if (action["set_tweak"_L1].toString() == u"highlight"_s) {
if (action["value"_L1].toString() != u"false"_s) {
highlightEnabled = true;
}
}
}
if (!enabled) {
return PushRuleAction::Off;
}
if (notify) {
if (isNoisy && highlightEnabled) {
return PushRuleAction::NoisyHighlight;
} else if (isNoisy) {
return PushRuleAction::Noisy;
} else if (highlightEnabled) {
return PushRuleAction::Highlight;
} else {
return PushRuleAction::On;
}
} else {
return PushRuleAction::Off;
}
}
QList<QVariant> PushRuleModel::actionToVariant(PushRuleAction::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 == PushRuleAction::Unknown) {
Q_ASSERT(false);
return QList<QVariant>();
}
QList<QVariant> actions;
if (action != PushRuleAction::Off) {
actions.append(u"notify"_s);
} else {
actions.append(u"dont_notify"_s);
}
if (action == PushRuleAction::Noisy || action == PushRuleAction::NoisyHighlight) {
QJsonObject soundTweak;
soundTweak.insert("set_tweak"_L1, u"sound"_s);
soundTweak.insert("value"_L1, sound);
actions.append(soundTweak);
}
if (action == PushRuleAction::Highlight || action == PushRuleAction::NoisyHighlight) {
QJsonObject highlightTweak;
highlightTweak.insert("set_tweak"_L1, u"highlight"_s);
actions.append(highlightTweak);
}
return actions;
}
NeoChatConnection *PushRuleModel::connection() const
{
return m_connection;
}
void PushRuleModel::setConnection(NeoChatConnection *connection)
{
if (connection == m_connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
if (m_connection) {
connect(m_connection, &NeoChatConnection::accountDataChanged, this, &PushRuleModel::updateNotificationRules);
updateNotificationRules(u"m.push_rules"_s);
}
}
#include "moc_pushrulemodel.cpp"

View File

@@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <Quotient/csapi/definitions/push_rule.h>
#include "enums/pushrule.h"
#include "neochatconnection.h"
using namespace Qt::StringLiterals;
/**
* @class PushRuleModel
*
* This class defines the model for managing notification push rule keywords.
*/
class PushRuleModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @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, PushRuleAction::Action
*/
Q_PROPERTY(bool globalNotificationsSet READ globalNotificationsSet NOTIFY globalNotificationsSetChanged)
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
struct Rule {
QString id;
PushRuleKind::Kind kind;
PushRuleAction::Action action;
PushRuleSection::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 PushRuleAction 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)
explicit PushRuleModel(QObject *parent = nullptr);
[[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<int, QByteArray> roleNames() const override;
Q_INVOKABLE void setPushRuleAction(const QString &id, PushRuleAction::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);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
Q_SIGNALS:
void globalNotificationsEnabledChanged();
void globalNotificationsSetChanged();
void connectionChanged();
private Q_SLOTS:
void updateNotificationRules(const QString &type);
private:
QList<Rule> m_rules;
QPointer<NeoChatConnection> m_connection;
void setRules(QList<Quotient::PushRule> rules, PushRuleKind::Kind kind);
int getRuleIndex(const QString &ruleId) const;
PushRuleSection::Section getSection(Quotient::PushRule rule);
void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled);
void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushRuleAction::Action action);
PushRuleAction::Action variantToAction(const QList<QVariant> &actions, bool enabled);
QList<QVariant> actionToVariant(PushRuleAction::Action action, const QString &sound = u"default"_s);
};
Q_DECLARE_METATYPE(PushRuleModel *)