diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/AliasEditModel.cpp | 336 | ||||
-rw-r--r-- | src/AliasEditModel.h | 79 | ||||
-rw-r--r-- | src/Cache.cpp | 10 | ||||
-rw-r--r-- | src/MainWindow.cpp | 8 | ||||
-rw-r--r-- | src/PowerlevelsEditModels.cpp | 7 | ||||
-rw-r--r-- | src/dbus/NhekoDBusBackend.cpp | 50 | ||||
-rw-r--r-- | src/timeline/InputBar.cpp | 56 | ||||
-rw-r--r-- | src/timeline/TimelineModel.cpp | 86 | ||||
-rw-r--r-- | src/timeline/TimelineModel.h | 8 | ||||
-rw-r--r-- | src/ui/NhekoGlobalObject.h | 5 |
10 files changed, 609 insertions, 36 deletions
diff --git a/src/AliasEditModel.cpp b/src/AliasEditModel.cpp new file mode 100644 index 00000000..aee42dd1 --- /dev/null +++ b/src/AliasEditModel.cpp @@ -0,0 +1,336 @@ +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "AliasEditModel.h" + +#include <QSharedPointer> + +#include <set> + +#include <mtx/responses/common.hpp> + +#include "Cache.h" +#include "Cache_p.h" +#include "ChatPage.h" +#include "Logging.h" +#include "MatrixClient.h" +#include "timeline/Permissions.h" +#include "timeline/TimelineModel.h" + +AliasEditingModel::AliasEditingModel(const std::string &rid, QObject *parent) + : QAbstractListModel(parent) + , room_id(rid) + , aliasEvent(cache::client() + ->getStateEvent<mtx::events::state::CanonicalAlias>(room_id) + .value_or(mtx::events::StateEvent<mtx::events::state::CanonicalAlias>{}) + .content) + , canSendStateEvent( + Permissions(QString::fromStdString(rid)).canChange(qml_mtx_events::CanonicalAlias)) +{ + std::set<std::string> seen_aliases; + + if (!aliasEvent.alias.empty()) { + aliases.push_back(Entry{aliasEvent.alias, true, true, false}); + seen_aliases.insert(aliasEvent.alias); + } + + for (const auto &alias : aliasEvent.alt_aliases) { + if (!seen_aliases.count(alias)) { + aliases.push_back(Entry{alias, false, true, false}); + seen_aliases.insert(alias); + } + } + + for (const auto &alias : qAsConst(aliases)) { + fetchAliasesStatus(alias.alias); + } + fetchPublishedAliases(); +} + +void +AliasEditingModel::fetchPublishedAliases() +{ + auto job = QSharedPointer<FetchPublishedAliasesJob>::create(); + connect(job.data(), + &FetchPublishedAliasesJob::advertizedAliasesFetched, + this, + &AliasEditingModel::updatePublishedAliases); + http::client()->list_room_aliases( + room_id, [job](const mtx::responses::Aliases &aliasesFetched, mtx::http::RequestErr) { + emit job->advertizedAliasesFetched(std::move(aliasesFetched.aliases)); + }); +} + +void +AliasEditingModel::fetchAliasesStatus(const std::string &alias) +{ + auto job = QSharedPointer<FetchPublishedAliasesJob>::create(); + connect( + job.data(), &FetchPublishedAliasesJob::aliasFetched, this, &AliasEditingModel::updateAlias); + http::client()->resolve_room_alias( + alias, [job, alias](const mtx::responses::RoomId &roomIdFetched, mtx::http::RequestErr e) { + if (!e) + emit job->aliasFetched(alias, std::move(roomIdFetched.room_id)); + }); +} + +QHash<int, QByteArray> +AliasEditingModel::roleNames() const +{ + return { + {Name, "name"}, + {IsPublished, "isPublished"}, + {IsCanonical, "isCanonical"}, + {IsAdvertized, "isAdvertized"}, + }; +} + +QVariant +AliasEditingModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= aliases.size()) + return {}; + + const auto &entry = aliases.at(index.row()); + + switch (role) { + case Name: + return QString::fromStdString(entry.alias); + case IsPublished: + return entry.published; + case IsCanonical: + return entry.canonical; + case IsAdvertized: + return entry.advertized; + } + + return {}; +} + +bool +AliasEditingModel::deleteAlias(int row) +{ + if (row < 0 || row >= aliases.size() || aliases.at(row).alias.empty()) + return false; + + auto alias = aliases.at(row); + + beginRemoveRows(QModelIndex(), row, row); + aliases.remove(row); + endRemoveRows(); + + if (alias.published) + http::client()->delete_room_alias(alias.alias, [alias](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to delete {}: {}", alias.alias, *e); + ChatPage::instance()->showNotification( + tr("Failed to unpublish alias %1: %2") + .arg(QString::fromStdString(alias.alias), + QString::fromStdString(e->matrix_error.error))); + } + }); + + if (aliasEvent.alias == alias.alias) + aliasEvent.alias.clear(); + + for (size_t i = 0; i < aliasEvent.alt_aliases.size(); i++) { + if (aliasEvent.alt_aliases[i] == alias.alias) { + aliasEvent.alt_aliases.erase(aliasEvent.alt_aliases.begin() + i); + break; + } + } + + return true; +} + +void +AliasEditingModel::addAlias(QString newAlias) +{ + const auto aliasStr = newAlias.toStdString(); + for (const auto &e : qAsConst(aliases)) { + if (e.alias == aliasStr) { + return; + } + } + + beginInsertRows(QModelIndex(), aliases.length(), aliases.length()); + if (aliasEvent.alias.empty()) + aliasEvent.alias = aliasStr; + else + aliasEvent.alt_aliases.push_back(aliasStr); + aliases.push_back( + Entry{aliasStr, aliasEvent.alias.empty() && canSendStateEvent, canSendStateEvent, false}); + endInsertRows(); + + auto job = QSharedPointer<FetchPublishedAliasesJob>::create(); + connect( + job.data(), &FetchPublishedAliasesJob::aliasFetched, this, &AliasEditingModel::updateAlias); + auto room = room_id; + http::client()->add_room_alias( + aliasStr, room_id, [job, aliasStr, room](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to publish {}: {}", aliasStr, *e); + ChatPage::instance()->showNotification( + tr("Failed to unpublish alias %1: %2") + .arg(QString::fromStdString(aliasStr), + QString::fromStdString(e->matrix_error.error))); + emit job->aliasFetched(aliasStr, ""); + } else { + emit job->aliasFetched(aliasStr, room); + } + }); +} + +void +AliasEditingModel::makeCanonical(int row) +{ + if (!canSendStateEvent || row < 0 || row >= aliases.size() || aliases.at(row).alias.empty()) + return; + + auto moveAlias = aliases.at(row).alias; + + if (!aliasEvent.alias.empty()) { + for (qsizetype i = 0; i < aliases.size(); i++) { + if (moveAlias == aliases[i].alias) { + if (aliases[i].canonical) { + aliases[i].canonical = false; + aliasEvent.alt_aliases.push_back(aliasEvent.alias); + emit dataChanged(index(i), index(i), {IsCanonical}); + } + break; + } + } + } + + aliasEvent.alias = moveAlias; + for (auto i = aliasEvent.alt_aliases.begin(); i != aliasEvent.alt_aliases.end(); ++i) { + if (*i == moveAlias) { + aliasEvent.alt_aliases.erase(i); + break; + } + } + aliases[row].canonical = true; + aliases[row].advertized = true; + emit dataChanged(index(row), index(row), {IsCanonical, IsAdvertized}); +} + +void +AliasEditingModel::togglePublish(int row) +{ + if (row < 0 || row >= aliases.size() || aliases.at(row).alias.empty()) + return; + auto aliasStr = aliases[row].alias; + + auto job = QSharedPointer<FetchPublishedAliasesJob>::create(); + connect( + job.data(), &FetchPublishedAliasesJob::aliasFetched, this, &AliasEditingModel::updateAlias); + auto room = room_id; + if (!aliases[row].published) + http::client()->add_room_alias( + aliasStr, room_id, [job, aliasStr, room](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to publish {}: {}", aliasStr, *e); + ChatPage::instance()->showNotification( + tr("Failed to unpublish alias %1: %2") + .arg(QString::fromStdString(aliasStr), + QString::fromStdString(e->matrix_error.error))); + emit job->aliasFetched(aliasStr, ""); + } else { + emit job->aliasFetched(aliasStr, room); + } + }); + else + http::client()->delete_room_alias(aliasStr, [job, aliasStr, room](mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to unpublish {}: {}", aliasStr, *e); + ChatPage::instance()->showNotification( + tr("Failed to unpublish alias %1: %2") + .arg(QString::fromStdString(aliasStr), + QString::fromStdString(e->matrix_error.error))); + emit job->aliasFetched(aliasStr, room); + } else { + emit job->aliasFetched(aliasStr, ""); + } + }); +} + +void +AliasEditingModel::toggleAdvertize(int row) +{ + if (!canSendStateEvent || row < 0 || row >= aliases.size() || aliases.at(row).alias.empty()) + return; + + auto &moveAlias = aliases[row]; + if (aliasEvent.alias == moveAlias.alias) { + moveAlias.canonical = false; + moveAlias.advertized = false; + aliasEvent.alias.clear(); + emit dataChanged(index(row), index(row), {IsAdvertized, IsCanonical}); + } else if (moveAlias.advertized) { + for (auto i = aliasEvent.alt_aliases.begin(); i != aliasEvent.alt_aliases.end(); ++i) { + if (*i == moveAlias.alias) { + aliasEvent.alt_aliases.erase(i); + moveAlias.advertized = false; + emit dataChanged(index(row), index(row), {IsAdvertized}); + break; + } + } + } else { + aliasEvent.alt_aliases.push_back(moveAlias.alias); + moveAlias.advertized = true; + emit dataChanged(index(row), index(row), {IsAdvertized}); + } +} + +void +AliasEditingModel::updateAlias(std::string alias, std::string target) +{ + for (qsizetype i = 0; i < aliases.size(); i++) { + auto &e = aliases[i]; + if (e.alias == alias) { + e.published = (target == room_id); + emit dataChanged(index(i), index(i), {IsPublished}); + } + } +} + +void +AliasEditingModel::updatePublishedAliases(std::vector<std::string> advAliases) +{ + for (const auto &advAlias : advAliases) { + bool found = false; + for (qsizetype i = 0; i < aliases.size(); i++) { + auto &alias = aliases[i]; + if (alias.alias == advAlias) { + alias.published = true; + emit dataChanged(index(i), index(i), {IsPublished}); + found = true; + break; + } + } + + if (!found) { + beginInsertRows(QModelIndex(), aliases.size(), aliases.size()); + aliases.push_back(Entry{advAlias, false, false, true}); + endInsertRows(); + } + } +} + +void +AliasEditingModel::commit() +{ + if (!canSendStateEvent) + return; + + http::client()->send_state_event( + room_id, aliasEvent, [](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) { + nhlog::net()->error("Failed to send Alias event: {}", *e); + ChatPage::instance()->showNotification( + tr("Failed to update aliases: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + } + }); +} diff --git a/src/AliasEditModel.h b/src/AliasEditModel.h new file mode 100644 index 00000000..4fccf9ce --- /dev/null +++ b/src/AliasEditModel.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <QAbstractListModel> +#include <QVector> + +#include <mtx/events/canonical_alias.hpp> + +#include "CacheStructs.h" + +class FetchPublishedAliasesJob : public QObject +{ + Q_OBJECT + +public: + explicit FetchPublishedAliasesJob(QObject *p = nullptr) + : QObject(p) + {} + +signals: + void aliasFetched(std::string alias, std::string target); + void advertizedAliasesFetched(std::vector<std::string> aliases); +}; + +class AliasEditingModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(bool canAdvertize READ canAdvertize CONSTANT) + +public: + enum Roles + { + Name, + IsPublished, + IsCanonical, + IsAdvertized, + }; + + explicit AliasEditingModel(const std::string &room_id_, QObject *parent = nullptr); + + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &) const override { return static_cast<int>(aliases.size()); } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + bool canAdvertize() const { return canSendStateEvent; } + + Q_INVOKABLE bool deleteAlias(int row); + Q_INVOKABLE void addAlias(QString newAlias); + Q_INVOKABLE void makeCanonical(int row); + Q_INVOKABLE void togglePublish(int row); + Q_INVOKABLE void toggleAdvertize(int row); + Q_INVOKABLE void commit(); + +private slots: + void updateAlias(std::string alias, std::string target); + void updatePublishedAliases(std::vector<std::string> aliases); + +private: + void fetchAliasesStatus(const std::string &alias); + void fetchPublishedAliases(); + + struct Entry + { + ~Entry() = default; + + std::string alias; + bool canonical = false; + bool advertized = false; + bool published = false; + }; + + std::string room_id; + QVector<Entry> aliases; + mtx::events::state::CanonicalAlias aliasEvent; + bool canSendStateEvent = false; +}; diff --git a/src/Cache.cpp b/src/Cache.cpp index 67889543..c9baaf5e 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -4442,11 +4442,15 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn, std::string_view oldKeys; - UserKeyCache cacheEntry; + UserKeyCache cacheEntry{}; auto res = db.get(txn, user, oldKeys); if (res) { - cacheEntry = nlohmann::json::parse(std::string_view(oldKeys.data(), oldKeys.size())) - .get<UserKeyCache>(); + try { + cacheEntry = nlohmann::json::parse(std::string_view(oldKeys.data(), oldKeys.size())) + .get<UserKeyCache>(); + } catch (std::exception &e) { + nhlog::db()->error("Failed to parse {}: {}", oldKeys, e.what()); + } } cacheEntry.last_changed = sync_token; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index f031c80f..d2e28277 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -10,6 +10,7 @@ #include <mtx/requests.hpp> #include <mtx/responses/login.hpp> +#include "AliasEditModel.h" #include "BlurhashProvider.h" #include "Cache.h" #include "Cache_p.h" @@ -179,6 +180,13 @@ MainWindow::registerQmlTypes() qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login"); qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration"); qmlRegisterType<HiddenEvents>("im.nheko", 1, 0, "HiddenEvents"); + qmlRegisterUncreatableType<AliasEditingModel>( + "im.nheko", + 1, + 0, + "AliasEditingModel", + QStringLiteral("Please use editAliases to create the models")); + qmlRegisterUncreatableType<PowerlevelEditingModels>( "im.nheko", 1, diff --git a/src/PowerlevelsEditModels.cpp b/src/PowerlevelsEditModels.cpp index 38ab42dc..fcfde26e 100644 --- a/src/PowerlevelsEditModels.cpp +++ b/src/PowerlevelsEditModels.cpp @@ -198,6 +198,13 @@ PowerlevelsTypeListModel::data(const QModelIndex &index, int role) const else if (type.type == "m.sticker") return tr("Send stickers"); + else if (type.type == "m.policy.rule.user") + return tr("Ban users using policy rules"); + else if (type.type == "m.policy.rule.room") + return tr("Ban rooms using policy rules"); + else if (type.type == "m.policy.rule.server") + return tr("Ban servers using policy rules"); + else if (type.type == "m.space.child") return tr("Edit child rooms"); else if (type.type == "m.space.parent") diff --git a/src/dbus/NhekoDBusBackend.cpp b/src/dbus/NhekoDBusBackend.cpp index 836475ee..9abc0433 100644 --- a/src/dbus/NhekoDBusBackend.cpp +++ b/src/dbus/NhekoDBusBackend.cpp @@ -4,6 +4,9 @@ #include "NhekoDBusBackend.h" +#include <mutex> + +#include "Cache.h" #include "Cache_p.h" #include "ChatPage.h" #include "Logging.h" @@ -18,15 +21,34 @@ NhekoDBusBackend::NhekoDBusBackend(RoomlistModel *parent) , m_parent{parent} {} +namespace { +struct RoomReplyState +{ + QVector<nheko::dbus::RoomInfoItem> model; + std::map<QString, RoomInfo> roominfos; + std::mutex m; +}; +} + QVector<nheko::dbus::RoomInfoItem> NhekoDBusBackend::rooms(const QDBusMessage &message) { + message.setDelayedReply(true); + nhlog::ui()->debug("Rooms requested over D-Bus."); + const auto roomListModel = m_parent->models; - QSharedPointer<QVector<nheko::dbus::RoomInfoItem>> model{ - new QVector<nheko::dbus::RoomInfoItem>}; + + auto state = QSharedPointer<RoomReplyState>::create(); + + std::vector<std::string> roomids; + roomids.reserve(roomids.size()); + for (const auto &room : roomListModel) { + roomids.push_back(room->roomId().toStdString()); + } + state->roominfos = cache::getRoomInfo(roomids); for (const auto &room : roomListModel) { - auto addRoom = [room, roomListModelSize = roomListModel.size(), message, model]( + auto addRoom = [room, roomListModelSize = roomListModel.size(), message, state]( const QImage &image) { const auto aliases = cache::client()->getStateEvent<mtx::events::state::CanonicalAlias>( room->roomId().toStdString()); @@ -39,24 +61,30 @@ NhekoDBusBackend::rooms(const QDBusMessage &message) alias = QString::fromStdString(val.alt_aliases.front()); } - model->push_back(nheko::dbus::RoomInfoItem{ - room->roomId(), alias, room->roomName(), image, room->notificationCount()}); + std::lock_guard<std::mutex> childLock(state->m); + state->model.push_back(nheko::dbus::RoomInfoItem{ + room->roomId(), + alias, + QString::fromStdString(state->roominfos[room->roomId()].name), + image, + room->notificationCount()}); - if (model->length() == roomListModelSize) { + if (state->model.size() == roomListModelSize) { + nhlog::ui()->debug("Sending {} rooms over D-Bus...", state->model.size()); auto reply = message.createReply(); - nhlog::ui()->debug("Sending {} rooms over D-Bus...", model->size()); - reply << QVariant::fromValue(*model); + reply << QVariant::fromValue(state->model); QDBusConnection::sessionBus().send(reply); nhlog::ui()->debug("Rooms successfully sent to D-Bus."); + } else { + // nhlog::ui()->debug("DBUS: {}/{}", state->model.size(), roomListModelSize); } }; - auto avatarUrl = room->roomAvatarUrl(); - if (avatarUrl.isEmpty()) + if (state->roominfos[room->roomId()].avatar_url.empty()) addRoom(QImage()); else MainWindow::instance()->imageProvider()->download( - avatarUrl.remove("mxc://"), + QString::fromStdString(state->roominfos[room->roomId()].avatar_url).remove("mxc://"), {96, 96}, [addRoom](const QString &, const QSize &, const QImage &image, const QString &) { addRoom(image); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index fe171deb..66bc8ef9 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -375,31 +375,37 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow } else if (!room->reply().isEmpty()) { auto related = room->relatedInfo(room->reply()); - QString body; - bool firstLine = true; - auto lines = related.quoted_body.splitRef(u'\n'); - for (auto line : qAsConst(lines)) { - if (firstLine) { - firstLine = false; - body = QStringLiteral("> <%1> %2\n").arg(related.quoted_user, line); - } else { - body += QStringLiteral("> %1\n").arg(line); + // Skip reply fallbacks to users who would cause a room ping with the fallback. + // This should be fine, since in some cases the reply fallback can be omitted now and the + // alternative is worse! On Element Android this applies to any substring, but that is their + // bug to fix. + if (!related.quoted_user.startsWith("@room:")) { + QString body; + bool firstLine = true; + auto lines = related.quoted_body.splitRef(u'\n'); + for (auto line : qAsConst(lines)) { + if (firstLine) { + firstLine = false; + body = QStringLiteral("> <%1> %2\n").arg(related.quoted_user, line); + } else { + body += QStringLiteral("> %1\n").arg(line); + } } - } - text.body = QStringLiteral("%1\n%2").arg(body, msg).toStdString(); + text.body = QStringLiteral("%1\n%2").arg(body, msg).toStdString(); - // NOTE(Nico): rich replies always need a formatted_body! - text.format = "org.matrix.custom.html"; - if ((ChatPage::instance()->userSettings()->markdown() && - useMarkdown == MarkdownOverride::NOT_SPECIFIED) || - useMarkdown == MarkdownOverride::ON) - text.formatted_body = - utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg, rainbowify)) - .toStdString(); - else - text.formatted_body = - utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString(); + // NOTE(Nico): rich replies always need a formatted_body! + text.format = "org.matrix.custom.html"; + if ((ChatPage::instance()->userSettings()->markdown() && + useMarkdown == MarkdownOverride::NOT_SPECIFIED) || + useMarkdown == MarkdownOverride::ON) + text.formatted_body = + utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg, rainbowify)) + .toStdString(); + else + text.formatted_body = + utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString(); + } text.relations.relations.push_back( {mtx::common::RelationType::InReplyTo, related.related_event}); @@ -692,6 +698,12 @@ InputBar::command(const QString &command, QString args) } else if (command == QLatin1String("unban")) { ChatPage::instance()->unbanUser( room->roomId(), args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == QLatin1String("redact")) { + if (args.startsWith('@')) { + room->redactAllFromUser(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (args.startsWith('$')) { + room->redactEvent(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } } else if (command == QLatin1String("roomnick")) { mtx::events::state::Member member; member.display_name = args.toStdString(); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 1a9f957b..db56ac52 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -190,6 +190,12 @@ qml_mtx_events::toRoomEventType(mtx::events::EventType e) return qml_mtx_events::EventType::Sticker; case EventType::Tag: return qml_mtx_events::EventType::Tag; + case EventType::PolicyRuleUser: + return qml_mtx_events::EventType::PolicyRuleUser; + case EventType::PolicyRuleRoom: + return qml_mtx_events::EventType::PolicyRuleRoom; + case EventType::PolicyRuleServer: + return qml_mtx_events::EventType::PolicyRuleServer; case EventType::SpaceParent: return qml_mtx_events::EventType::SpaceParent; case EventType::SpaceChild: @@ -303,6 +309,12 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t) // m.tag case qml_mtx_events::Tag: return mtx::events::EventType::Tag; + case qml_mtx_events::PolicyRuleUser: + return mtx::events::EventType::PolicyRuleUser; + case qml_mtx_events::PolicyRuleRoom: + return mtx::events::EventType::PolicyRuleRoom; + case qml_mtx_events::PolicyRuleServer: + return mtx::events::EventType::PolicyRuleServer; // m.space.parent case qml_mtx_events::SpaceParent: return mtx::events::EventType::SpaceParent; @@ -1282,6 +1294,24 @@ TimelineModel::showReadReceipts(QString id) } void +TimelineModel::redactAllFromUser(const QString &userid, const QString &reason) +{ + auto user = userid.toStdString(); + std::vector<QString> toRedact; + for (auto it = events.size() - 1; it >= 0; --it) { + auto event = events.get(it, false); + if (event && mtx::accessors::sender(*event) == user && + !std::holds_alternative<mtx::events::RoomEvent<mtx::events::msg::Redacted>>(*event)) { + toRedact.push_back(QString::fromStdString(mtx::accessors::event_id(*event))); + } + } + + for (const auto &e : toRedact) { + redactEvent(e, reason); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } +} +void TimelineModel::redactEvent(const QString &id, const QString &reason) { if (!id.isEmpty()) { @@ -2321,6 +2351,62 @@ TimelineModel::formatImagePackEvent(const QString &id) return msg; } +QString +TimelineModel::formatPolicyRule(const QString &id) +{ + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return {}; + + auto qsHtml = [](const std::string &s) { return QString::fromStdString(s).toHtmlEscaped(); }; + constexpr std::string_view unstable_ban = "org.matrix.mjolnir.ban"; + + if (auto userRule = + std::get_if<mtx::events::StateEvent<mtx::events::state::policy_rule::UserRule>>(e)) { + auto sender = utils::replaceEmoji(displayName(QString::fromStdString(userRule->sender))); + if (userRule->content.entity.empty() || + (userRule->content.recommendation != + mtx::events::state::policy_rule::recommendation::ban && + userRule->content.recommendation != unstable_ban)) { + return tr("%1 disabled the rule to ban users matching %2.") + .arg(sender, qsHtml(userRule->content.entity)); + } else { + return tr("%1 added a rule to ban users matching %2 for '%3'.") + .arg(sender, qsHtml(userRule->content.entity), qsHtml(userRule->content.reason)); + } + } else if (auto roomRule = + std::get_if<mtx::events::StateEvent<mtx::events::state::policy_rule::RoomRule>>( + e)) { + auto sender = utils::replaceEmoji(displayName(QString::fromStdString(roomRule->sender))); + if (roomRule->content.entity.empty() || + (roomRule->content.recommendation != + mtx::events::state::policy_rule::recommendation::ban && + roomRule->content.recommendation != unstable_ban)) { + return tr("%1 disabled the rule to ban rooms matching %2.") + .arg(sender, qsHtml(roomRule->content.entity)); + } else { + return tr("%1 added a rule to ban rooms matching %2 for '%3'.") + .arg(sender, qsHtml(roomRule->content.entity), qsHtml(roomRule->content.reason)); + } + } else if (auto serverRule = + std::get_if<mtx::events::StateEvent<mtx::events::state::policy_rule::ServerRule>>( + e)) { + auto sender = utils::replaceEmoji(displayName(QString::fromStdString(serverRule->sender))); + if (serverRule->content.entity.empty() || + (serverRule->content.recommendation != + mtx::events::state::policy_rule::recommendation::ban && + serverRule->content.recommendation != unstable_ban)) { + return tr("%1 disabled the rule to ban servers matching %2.") + .arg(sender, qsHtml(serverRule->content.entity)); + } else { + return tr("%1 added a rule to ban servers matching %2 for '%3'.") + .arg(sender, qsHtml(serverRule->content.entity), qsHtml(serverRule->content.reason)); + } + } + + return {}; +} + QVariantMap TimelineModel::formatRedactedEvent(const QString &id) { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 3b954394..6d424981 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -116,6 +116,12 @@ enum EventType ImagePackInAccountData, //! m.image_pack.rooms, currently im.ponies.emote_rooms ImagePackRooms, + // m.policy.rule.user + PolicyRuleUser, + // m.policy.rule.room + PolicyRuleRoom, + // m.policy.rule.server + PolicyRuleServer, // m.space.parent SpaceParent, // m.space.child @@ -264,6 +270,7 @@ public: Q_INVOKABLE QString formatGuestAccessEvent(const QString &id); Q_INVOKABLE QString formatPowerLevelEvent(const QString &id); Q_INVOKABLE QString formatImagePackEvent(const QString &id); + Q_INVOKABLE QString formatPolicyRule(const QString &id); Q_INVOKABLE QVariantMap formatRedactedEvent(const QString &id); Q_INVOKABLE void viewRawMessage(const QString &id); @@ -276,6 +283,7 @@ public: Q_INVOKABLE void pin(const QString &id); Q_INVOKABLE void showReadReceipts(QString id); Q_INVOKABLE void redactEvent(const QString &id, const QString &reason = ""); + Q_INVOKABLE void redactAllFromUser(const QString &userid, const QString &reason = ""); Q_INVOKABLE int idToIndex(const QString &id) const; Q_INVOKABLE QString indexToId(int index) const; Q_INVOKABLE void openMedia(const QString &eventId); diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index bd141f35..f9de489d 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -9,6 +9,7 @@ #include <QObject> #include <QPalette> +#include "AliasEditModel.h" #include "PowerlevelsEditModels.h" #include "Theme.h" #include "UserProfile.h" @@ -59,6 +60,10 @@ public: { return new PowerlevelEditingModels(room_id_); } + Q_INVOKABLE AliasEditingModel *editAliases(QString room_id_) const + { + return new AliasEditingModel(room_id_.toStdString()); + } public slots: void updateUserProfile(); |