From b5ce330c825b0ff7ddeeabfba57bee664bba88f6 Mon Sep 17 00:00:00 2001 From: Integral Date: Mon, 9 Feb 2026 17:44:51 +0800 Subject: [PATCH 1/9] Update `toggleAction_` text when parent window visibility changes Currently, the `toggleAction_` text is only updated when the parent window visibility changes via the tray icon menu. If the visibility changes through other means, the action text becomes out of sync. Connect parent window visibility changes to `toggleAction_` to keep the text in sync. --- src/TrayIcon.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp index 1c2f7fb5..4f38c63d 100644 --- a/src/TrayIcon.cpp +++ b/src/TrayIcon.cpp @@ -110,14 +110,11 @@ TrayIcon::TrayIcon(const QString &filename, QWindow *parent) toggleAction_ = new QAction(tr("Show"), this); quitAction_ = new QAction(tr("Quit"), this); - connect(toggleAction_, &QAction::triggered, parent, [=, this]() { - if (parent->isVisible()) { - parent->hide(); - toggleAction_->setText(tr("Show")); - } else { - parent->show(); - toggleAction_->setText(tr("Hide")); - } + connect(parent, &QWindow::visibleChanged, toggleAction_, [=, this] { + toggleAction_->setText(tr(parent->isVisible() ? "Hide" : "Show")); + }); + connect(toggleAction_, &QAction::triggered, parent, [=] { + parent->isVisible() ? parent->hide() : parent->show(); }); connect(quitAction_, &QAction::triggered, this, QApplication::quit); From e3bc05884565613c3b0ef3425d966fada8446cc9 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 20 Feb 2026 01:32:37 +0100 Subject: [PATCH 2/9] Refactor v12 support to use new user_level helper from mtxclient --- CMakeLists.txt | 2 +- im.nheko.Nheko.yaml | 4 +- resources/qml/TimelineSectionHeader.qml | 3 +- .../qml/components/PowerlevelIndicator.qml | 11 +-- resources/qml/dialogs/PowerLevelEditor.qml | 26 ++++-- resources/qml/dialogs/RoomMembers.qml | 1 - src/Cache.cpp | 89 ++++--------------- src/Cache.h | 4 - src/Cache_p.h | 4 - src/MemberList.cpp | 29 +----- src/MemberList.h | 2 +- src/PowerlevelsEditModels.cpp | 78 +++++++++++----- src/PowerlevelsEditModels.h | 30 +++++-- src/Utils.cpp | 47 ++++++++-- src/timeline/Permissions.cpp | 39 ++++---- src/timeline/Permissions.h | 10 ++- src/timeline/TimelineModel.cpp | 35 ++++---- src/timeline/TimelineModel.h | 1 - 18 files changed, 208 insertions(+), 207 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b76c488..0976231b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -622,7 +622,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG d6f10427d1c5e5b1a45f426274f8d2e8dd0b64be + GIT_TAG 873911e352a0845dfb178f77b1ddea796a5d3455 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/im.nheko.Nheko.yaml b/im.nheko.Nheko.yaml index b7bc6dee..76f15bb9 100644 --- a/im.nheko.Nheko.yaml +++ b/im.nheko.Nheko.yaml @@ -213,8 +213,8 @@ modules: - -DBUILD_SHARED_LIBS=OFF buildsystem: cmake-ninja sources: - - commit: 15b43844f4ec27faa5f2ec92c4ded313206763aa - tag: v0.10.1 + - commit: 873911e352a0845dfb178f77b1ddea796a5d3455 + #tag: v0.10.1 type: git url: https://github.com/Nheko-Reborn/mtxclient.git - name: nheko diff --git a/resources/qml/TimelineSectionHeader.qml b/resources/qml/TimelineSectionHeader.qml index f504c463..2a95807c 100644 --- a/resources/qml/TimelineSectionHeader.qml +++ b/resources/qml/TimelineSectionHeader.qml @@ -89,7 +89,6 @@ Column { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - isV12Creator: room.isV12Creator(userId) powerlevel: userPowerlevel height: fontMetrics.ascent width: height @@ -98,7 +97,7 @@ Column { sourceSize.height: height permissions: room ? room.permissions : null - visible: isAdmin || isModerator || isV12Creator + visible: isAdmin || isModerator // implicitly includes creators as well } ToolTip.delay: Nheko.tooltipDelay diff --git a/resources/qml/components/PowerlevelIndicator.qml b/resources/qml/components/PowerlevelIndicator.qml index 07453828..00942aec 100644 --- a/resources/qml/components/PowerlevelIndicator.qml +++ b/resources/qml/components/PowerlevelIndicator.qml @@ -7,10 +7,10 @@ import QtQuick.Controls import im.nheko Image { - required property int powerlevel + required property var powerlevel required property var permissions - required property bool isV12Creator + readonly property bool isV12Creator: permissions ? permissions.creatorLevel() == powerlevel : false readonly property bool isAdmin: permissions ? permissions.changeLevel(MtxEvent.PowerLevels) <= powerlevel : false readonly property bool isModerator: permissions ? permissions.redactLevel() <= powerlevel : false readonly property bool isDefault: permissions ? permissions.defaultLevel() <= powerlevel : false @@ -27,14 +27,15 @@ Image { source: sourceUrl + (ma.hovered ? palette.highlight : palette.buttonText) ToolTip.visible: ma.hovered ToolTip.text: { + let pl = powerlevel.toLocaleString(Qt.locale(), "f", 0); if (isV12Creator) return qsTr("Creator"); else if (isAdmin) - return qsTr("Administrator: %1").arg(powerlevel); + return qsTr("Administrator (%1)").arg(pl) else if (isModerator) - return qsTr("Moderator: %1").arg(powerlevel); + return qsTr("Moderator: %1").arg(pl); else - return qsTr("User: %1").arg(powerlevel); + return qsTr("User: %1").arg(pl); } HoverHandler { diff --git a/resources/qml/dialogs/PowerLevelEditor.qml b/resources/qml/dialogs/PowerLevelEditor.qml index 17b19c25..694a259e 100644 --- a/resources/qml/dialogs/PowerLevelEditor.qml +++ b/resources/qml/dialogs/PowerLevelEditor.qml @@ -94,14 +94,17 @@ ApplicationWindow { Text { visible: !model.isType; text: { + let pl = model.powerlevel.toLocaleString(Qt.locale(), "f", 0); + if (editingModel.creatorLevel == model.powerlevel) + return qsTr("Creator") if (editingModel.adminLevel == model.powerlevel) - return qsTr("Administrator (%1)").arg(model.powerlevel) + return qsTr("Administrator (%1)").arg(pl) else if (editingModel.moderatorLevel == model.powerlevel) - return qsTr("Moderator (%1)").arg(model.powerlevel) + return qsTr("Moderator (%1)").arg(pl) else if (editingModel.defaultUserLevel == model.powerlevel) - return qsTr("User (%1)").arg(model.powerlevel) + return qsTr("User (%1)").arg(pl) else - return qsTr("Custom (%1)").arg(model.powerlevel) + return qsTr("Custom (%1)").arg(pl) } color: palette.text } @@ -138,7 +141,7 @@ ApplicationWindow { color: palette.text - Keys.onPressed: { + Keys.onPressed: event => { if (typeEntry.text.includes('.') && event.matches(StandardKey.InsertParagraphSeparator)) { editingModel.types.add(typeEntry.index, typeEntry.text) typeEntry.visible = false; @@ -334,12 +337,17 @@ ApplicationWindow { Text { visible: !model.isUser; text: { + let pl = model.powerlevel.toLocaleString(Qt.locale(), "f", 0); + if (editingModel.creatorLevel == model.powerlevel) + return qsTr("Creator") if (editingModel.adminLevel == model.powerlevel) - return qsTr("Administrator (%1)").arg(model.powerlevel) + return qsTr("Administrator (%1)").arg(pl) else if (editingModel.moderatorLevel == model.powerlevel) - return qsTr("Moderator (%1)").arg(model.powerlevel) + return qsTr("Moderator (%1)").arg(pl) + else if (editingModel.defaultUserLevel == model.powerlevel) + return qsTr("User (%1)").arg(pl) else - return qsTr("Custom (%1)").arg(model.powerlevel) + return qsTr("Custom (%1)").arg(pl) } color: palette.text } @@ -349,7 +357,7 @@ ApplicationWindow { Layout.alignment: Qt.AlignRight Layout.rightMargin: 2 image: model.isUser ? ":/icons/icons/ui/dismiss.svg" : ":/icons/icons/ui/add-square-button.svg" - visible: !model.isUser || model.removeable + visible: (!model.isUser || model.removeable) && model.powerlevel != editingModel.creatorLevel hoverEnabled: true ToolTip.visible: hovered ToolTip.text: model.isUser ? qsTr("Remove user") : qsTr("Add user") diff --git a/resources/qml/dialogs/RoomMembers.qml b/resources/qml/dialogs/RoomMembers.qml index f03fda96..95dc9fc3 100644 --- a/resources/qml/dialogs/RoomMembers.qml +++ b/resources/qml/dialogs/RoomMembers.qml @@ -168,7 +168,6 @@ ApplicationWindow { sourceSize.height: height powerlevel: model.powerlevel permissions: room.permissions - isV12Creator: room.isV12Creator(model.mxid) } EncryptionIndicator { diff --git a/src/Cache.cpp b/src/Cache.cpp index ca8644ba..ee981abc 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -4621,12 +4621,14 @@ Cache::updateSpaces(lmdb::txn &txn, event.state_key.at(0) == '!') { const std::string &space = event.state_key; + auto create = getStateEvent(txn, space) + .value_or(mtx::events::StateEvent{}); auto pls = getStateEvent(txn, space); if (!pls) continue; - if (pls->content.user_level(event.sender) >= + if (pls->content.user_level(event.sender, create) >= pls->content.state_level(space_event_type)) { db->spacesChildren.put(txn, space, room); db->spacesParents.put(txn, room, space); @@ -4635,7 +4637,7 @@ Cache::updateSpaces(lmdb::txn &txn, room, space, event.sender, - pls->content.user_level(event.sender), + pls->content.user_level(event.sender, create), pls->content.state_level(space_event_type)); } } @@ -4851,51 +4853,6 @@ Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::st return std::nullopt; } -bool -Cache::isV12Creator(const std::string &room_id, const std::string &user_id) -{ - auto txn = ro_txn(this->db->env_); - auto state = this->getStatesDb(txn, room_id); - - return this->isV12Creator(txn, state, user_id); -} - -bool -Cache::isV12Creator(lmdb::txn &txn, lmdb::dbi &state, const std::string &user_id) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - bool ok; - const int room_version = this->getRoomVersion(txn, state).toInt(&ok); - if (!ok || room_version < 12) { - return false; - } - - std::string_view create_event; - if (state.get(txn, to_string(EventType::RoomCreate), create_event)) { - try { - const StateEvent evt = - nlohmann::json::parse(create_event).get>(); - if (evt.sender == user_id) { - return true; - } - - const std::optional> &additional_creators = - evt.content.additional_creators; - if (additional_creators && - std::find(additional_creators->begin(), additional_creators->end(), user_id) != - additional_creators->end()) { - return true; - } - } catch (...) { - return false; - } - } - - return false; -} - bool Cache::hasEnoughPowerLevel(const std::vector &eventTypes, const std::string &room_id, @@ -4908,30 +4865,22 @@ Cache::hasEnoughPowerLevel(const std::vector &eventTypes try { auto db_ = getStatesDb(txn, room_id); - if (this->isV12Creator(txn, db_, user_id)) { - return true; - } - int64_t min_event_level = std::numeric_limits::max(); int64_t user_level = std::numeric_limits::min(); - std::string_view event; - bool res = db_.get(txn, to_string(EventType::RoomPowerLevels), event); + try { + StateEvent create = getStateEvent(txn, room_id) + .value_or(StateEvent{}); + StateEvent pls = + getStateEvent(txn, room_id) + .value_or(StateEvent{}); - if (res) { - try { - StateEvent msg = - nlohmann::json::parse(std::string_view(event.data(), event.size())) - .get>(); + user_level = pls.content.user_level(user_id, create); - user_level = msg.content.user_level(user_id); - - for (const auto &ty : eventTypes) - min_event_level = - std::min(min_event_level, msg.content.state_level(to_string(ty))); - } catch (const nlohmann::json::exception &e) { - nhlog::db()->warn("failed to parse m.room.power_levels event: {}", e.what()); - } + for (const auto &ty : eventTypes) + min_event_level = std::min(min_event_level, pls.content.state_level(to_string(ty))); + } catch (const nlohmann::json::exception &e) { + nhlog::db()->warn("failed to parse m.room.power_levels event: {}", e.what()); } return user_level >= min_event_level; @@ -6178,13 +6127,6 @@ roomMembers(const std::string &room_id) return instance_->roomMembers(room_id); } -//! Check if the given user is a room creator and that gives them an infinite PL. -bool -isV12Creator(const std::string &room_id, const std::string &user_id) -{ - return instance_->isV12Creator(room_id, user_id); -} - //! Check if the given user has power level greater than //! lowest power level of the given events. bool @@ -6466,6 +6408,7 @@ NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::JoinRules) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Name) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::PinnedEvents) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::PowerLevels) +NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Create) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::ServerAcl) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::space::Child) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::space::Parent) diff --git a/src/Cache.h b/src/Cache.h index c6118c78..7c79881c 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -123,10 +123,6 @@ runMigrations(); std::vector roomMembers(const std::string &room_id); -//! Check if the given user is a room creator and that gives them an infinite PL. -bool -isV12Creator(const std::string &room_id, const std::string &user_id); - //! Check if the given user has power level greater than than //! lowest power level of the given events. bool diff --git a/src/Cache_p.h b/src/Cache_p.h index ee7913d5..89fdd843 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -145,10 +145,6 @@ public: //! Retrieve all the user ids from a room. std::vector roomMembers(const std::string &room_id); - //! Check if the given user is a room creator and that gives them an infinite PL. - bool isV12Creator(const std::string &room_id, const std::string &user_id); - bool isV12Creator(lmdb::txn &txn, lmdb::dbi &state, const std::string &user_id); - //! Check if the given user has power leve greater than than //! lowest power level of the given events. bool hasEnoughPowerLevel(const std::vector &eventTypes, diff --git a/src/MemberList.cpp b/src/MemberList.cpp index bc493165..10487974 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -19,6 +19,9 @@ MemberListBackend::MemberListBackend(const QString &room_id, QObject *parent) ->getStateEvent(room_id_.toStdString()) .value_or(mtx::events::StateEvent{}) .content} + , create_{cache::client() + ->getStateEvent(room_id_.toStdString()) + .value_or(mtx::events::StateEvent{})} { try { info_ = cache::singleRoomInfo(room_id_.toStdString()); @@ -92,7 +95,7 @@ MemberListBackend::data(const QModelIndex &index, int role) const } case Powerlevel: return static_cast( - powerLevels_.user_level(m_memberList[index.row()].first.user_id.toStdString())); + powerLevels_.user_level(m_memberList[index.row()].first.user_id.toStdString(), create_)); default: return {}; } @@ -172,28 +175,4 @@ MemberList::filterAcceptsRow(int source_row, const QModelIndex &) const Qt::CaseInsensitive); } -bool -MemberList::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const -{ - if (this->sortRole() != MemberSortRoles::Powerlevel) { - return QSortFilterProxyModel::lessThan(source_left, source_right); - } - - const QString &left = - this->m_model.data(source_left, MemberListBackend::Roles::Mxid).toString(); - const QString &right = - this->m_model.data(source_right, MemberListBackend::Roles::Mxid).toString(); - - const std::string &room_id = this->roomId().toStdString(); - if (cache::isV12Creator(room_id, left.toStdString())) { - if (!cache::isV12Creator(room_id, right.toStdString())) { - return false; - } - // If both are creators, sort by mxid. - return left < right; - } - - return QSortFilterProxyModel::lessThan(source_left, source_right); -} - #include "moc_MemberList.cpp" diff --git a/src/MemberList.h b/src/MemberList.h index 46d73b53..0ceaad5e 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -73,6 +73,7 @@ private: bool loadingMoreMembers_{false}; mtx::events::state::PowerLevels powerLevels_; + mtx::events::StateEvent create_; friend class MemberList; }; @@ -122,7 +123,6 @@ public slots: protected: bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; - bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; private: QString filterString; diff --git a/src/PowerlevelsEditModels.cpp b/src/PowerlevelsEditModels.cpp index 2ecbdd53..cf8a6944 100644 --- a/src/PowerlevelsEditModels.cpp +++ b/src/PowerlevelsEditModels.cpp @@ -17,12 +17,15 @@ #include "Logging.h" #include "MatrixClient.h" -PowerlevelsTypeListModel::PowerlevelsTypeListModel(const std::string &rid, - const mtx::events::state::PowerLevels &pl, - QObject *parent) +PowerlevelsTypeListModel::PowerlevelsTypeListModel( + const std::string &rid, + const mtx::events::state::PowerLevels &pl, + const mtx::events::StateEvent &create, + QObject *parent) : QAbstractListModel(parent) , room_id(rid) , powerLevels_(pl) + , create_(create) { std::set seen_levels; for (const auto &[type, level] : powerLevels_.events) { @@ -40,6 +43,9 @@ PowerlevelsTypeListModel::PowerlevelsTypeListModel(const std::string &rid, seen_levels.insert(level); } } + if (create_.content.room_version_creators_with_infinite_power()) { + seen_levels.insert(mtx::events::state::Creator); + } for (const auto &level : { powerLevels_.events_default, @@ -354,12 +360,15 @@ PowerlevelsTypeListModel::moveRows(const QModelIndex &, return true; } -PowerlevelsUserListModel::PowerlevelsUserListModel(const std::string &rid, - const mtx::events::state::PowerLevels &pl, - QObject *parent) +PowerlevelsUserListModel::PowerlevelsUserListModel( + const std::string &rid, + const mtx::events::state::PowerLevels &pl, + const mtx::events::StateEvent &create, + QObject *parent) : QAbstractListModel(parent) , room_id(rid) , powerLevels_(pl) + , create_(create) { std::set seen_levels; for (const auto &[user, level] : powerLevels_.users) { @@ -378,6 +387,16 @@ PowerlevelsUserListModel::PowerlevelsUserListModel(const std::string &rid, } } + if (create_.content.room_version_creators_with_infinite_power()) { + users.push_back(Entry{"", mtx::events::state::Creator}); + seen_levels.insert(mtx::events::state::Creator); + + users.push_back(Entry{create_.sender, mtx::events::state::Creator}); + for (const auto &user : create.content.additional_creators) { + users.push_back(Entry{user, mtx::events::state::Creator}); + } + } + for (const auto &level : { powerLevels_.events_default, powerLevels_.state_default, @@ -408,7 +427,7 @@ PowerlevelsUserListModel::toUsers() const { std::map> m; for (const auto &[key, pl] : std::as_const(users)) - if (key.size() > 0 && key.at(0) == '@') + if (key.size() > 0 && key.at(0) == '@' && pl != mtx::events::state::Creator) m[key] = pl; return m; } @@ -459,7 +478,7 @@ PowerlevelsUserListModel::data(const QModelIndex &index, int role) const case IsUser: return !user.mxid.empty(); case Moveable: - return !user.mxid.empty(); + return !user.mxid.empty() && user.pl != mtx::events::state::Creator; case Removeable: return !user.mxid.empty() && user.mxid.find('.') != std::string::npos; } @@ -554,7 +573,15 @@ PowerlevelsUserListModel::moveRows(const QModelIndex &, if (users.at(sourceRow).mxid.empty()) return false; - auto pl = users.at(destinationChild > 0 ? destinationChild - 1 : 0).pl; + if (users.at(sourceRow).pl == mtx::events::state::Creator) + return false; + if (users.at(destinationChild).pl == mtx::events::state::Creator) + return false; + + auto pl = users.at(destinationChild > 0 ? destinationChild - 1 : 0).pl; + if (pl == mtx::events::state::Creator) + return false; + auto sourceItem = users.takeAt(sourceRow); sourceItem.pl = pl; @@ -577,9 +604,12 @@ PowerlevelEditingModels::PowerlevelEditingModels(QString room_id, QObject *paren ->getStateEvent(room_id.toStdString()) .value_or(mtx::events::StateEvent{}) .content) - , types_(room_id.toStdString(), powerLevels_, this) - , users_(room_id.toStdString(), powerLevels_, this) - , spaces_(room_id.toStdString(), powerLevels_, this) + , create_(cache::client() + ->getStateEvent(room_id.toStdString()) + .value_or(mtx::events::StateEvent{})) + , types_(room_id.toStdString(), powerLevels_, create_, this) + , users_(room_id.toStdString(), powerLevels_, create_, this) + , spaces_(room_id.toStdString(), powerLevels_, create_, this) , room_id_(room_id.toStdString()) { connect(&types_, @@ -678,16 +708,18 @@ samePl(const mtx::events::state::PowerLevels &a, const mtx::events::state::Power b.redact); } -PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(const std::string &room_id_, - const mtx::events::state::PowerLevels &pl, - QObject *parent) +PowerlevelsSpacesListModel::PowerlevelsSpacesListModel( + const std::string &room_id_, + const mtx::events::state::PowerLevels &pl, + const mtx::events::StateEvent &create, + QObject *parent) : QAbstractListModel(parent) , room_id(std::move(room_id_)) , oldPowerLevels_(std::move(pl)) { beginResetModel(); - spaces.push_back(Entry{room_id, oldPowerLevels_, true}); + spaces.push_back(Entry{room_id, oldPowerLevels_, create, true}); std::unordered_set visited; @@ -703,10 +735,16 @@ PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(const std::string &room_i cache::client()->getStateEvent(s, space); if (parent && parent->content.via && !parent->content.via->empty() && parent->content.canonical) { - auto parentPl = cache::client()->getStateEvent(s); + auto childPl = cache::client()->getStateEvent(s); + auto childCreate = + cache::client()->getStateEvent(s).value_or( + mtx::events::StateEvent{}); - spaces.push_back(Entry{ - s, parentPl ? parentPl->content : mtx::events::state::PowerLevels{}, false}); + spaces.push_back( + Entry{s, + childPl ? childPl->content : mtx::events::state::PowerLevels{}, + childCreate, + false}); addChildren(s); } } @@ -813,7 +851,7 @@ PowerlevelsSpacesListModel::data(QModelIndex const &index, int role) const auto entry = spaces.at(row); switch (role) { case Roles::IsEditable: - return entry.pl.user_level(http::client()->user_id().to_string()) >= + return entry.pl.user_level(http::client()->user_id().to_string(), entry.create) >= entry.pl.state_level(to_string(mtx::events::EventType::RoomPowerLevels)); case Roles::IsDifferentFromBase: return !samePl(entry.pl, oldPowerLevels_); diff --git a/src/PowerlevelsEditModels.h b/src/PowerlevelsEditModels.h index 1fe075b7..edb3a821 100644 --- a/src/PowerlevelsEditModels.h +++ b/src/PowerlevelsEditModels.h @@ -29,9 +29,11 @@ public: Removeable, }; - explicit PowerlevelsTypeListModel(const std::string &room_id_, - const mtx::events::state::PowerLevels &pl, - QObject *parent = nullptr); + explicit PowerlevelsTypeListModel( + const std::string &room_id_, + const mtx::events::state::PowerLevels &pl, + const mtx::events::StateEvent &create, + QObject *parent = nullptr); QHash roleNames() const override; int rowCount(const QModelIndex &) const override { return static_cast(types.size()); } @@ -67,6 +69,7 @@ public: std::string room_id; QVector types; mtx::events::state::PowerLevels powerLevels_; + mtx::events::StateEvent create_; }; class PowerlevelsUserListModel final : public QAbstractListModel @@ -88,9 +91,11 @@ public: Removeable, }; - explicit PowerlevelsUserListModel(const std::string &room_id_, - const mtx::events::state::PowerLevels &pl, - QObject *parent = nullptr); + explicit PowerlevelsUserListModel( + const std::string &room_id_, + const mtx::events::state::PowerLevels &pl, + const mtx::events::StateEvent &create, + QObject *parent = nullptr); QHash roleNames() const override; int rowCount(const QModelIndex &) const override { return static_cast(users.size()); } @@ -121,6 +126,7 @@ public: std::string room_id; QVector users; mtx::events::state::PowerLevels powerLevels_; + mtx::events::StateEvent create_; }; class PowerlevelsSpacesListModel final : public QAbstractListModel @@ -147,9 +153,11 @@ public: ApplyPermissions, }; - explicit PowerlevelsSpacesListModel(const std::string &room_id_, - const mtx::events::state::PowerLevels &pl, - QObject *parent = nullptr); + explicit PowerlevelsSpacesListModel( + const std::string &room_id_, + const mtx::events::state::PowerLevels &pl, + const mtx::events::StateEvent &create, + QObject *parent = nullptr); QHash roleNames() const override; int rowCount(const QModelIndex &) const override { return static_cast(spaces.size()); } @@ -183,6 +191,7 @@ public: std::string roomid; mtx::events::state::PowerLevels pl; + mtx::events::StateEvent create; bool apply = false; }; @@ -203,6 +212,7 @@ class PowerlevelEditingModels final : public QObject Q_PROPERTY(PowerlevelsUserListModel *users READ users CONSTANT) Q_PROPERTY(PowerlevelsTypeListModel *types READ types CONSTANT) Q_PROPERTY(PowerlevelsSpacesListModel *spaces READ spaces CONSTANT) + Q_PROPERTY(qlonglong creatorLevel READ creatorLevel CONSTANT) Q_PROPERTY(qlonglong adminLevel READ adminLevel NOTIFY adminLevelChanged) Q_PROPERTY(qlonglong moderatorLevel READ moderatorLevel NOTIFY moderatorLevelChanged) Q_PROPERTY(qlonglong defaultUserLevel READ defaultUserLevel NOTIFY defaultUserLevelChanged) @@ -222,6 +232,7 @@ public: PowerlevelsUserListModel *users() { return &users_; } PowerlevelsTypeListModel *types() { return &types_; } PowerlevelsSpacesListModel *spaces() { return &spaces_; } + qlonglong creatorLevel() const { return mtx::events::state::Creator; } qlonglong adminLevel() const { return powerLevels_.state_level(to_string(mtx::events::EventType::RoomPowerLevels)); @@ -235,6 +246,7 @@ public: Q_INVOKABLE void addRole(int pl); mtx::events::state::PowerLevels powerLevels_; + mtx::events::StateEvent create_; PowerlevelsTypeListModel types_; PowerlevelsUserListModel users_; PowerlevelsSpacesListModel spaces_; diff --git a/src/Utils.cpp b/src/Utils.cpp index 2bec9d36..4556b680 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -1453,6 +1453,9 @@ utils::roomVias(const std::string &roomid) auto powerlevels = cache::client()->getStateEvent(roomid).value_or( mtx::events::StateEvent{}); + auto create = + cache::client()->getStateEvent(roomid).value_or( + mtx::events::StateEvent{}); auto acls = cache::client()->getStateEvent(roomid); std::vector allowedServers; @@ -1501,6 +1504,19 @@ utils::roomVias(const std::string &roomid) std::set users_with_high_pl_in_room; // we should pick PL > 50, but imo that is broken, so we just pick users who have admins // perm + if (create.content.room_version_creators_with_infinite_power()) { + { + auto user = create.sender; + auto host = mtx::identifiers::parse(user).hostname(); + if (isHostAllowed(host)) + users_with_high_pl.insert(user); + } + for (const auto &user : create.content.additional_creators) { + auto host = mtx::identifiers::parse(user).hostname(); + if (isHostAllowed(host)) + users_with_high_pl.insert(user); + } + } for (const auto &user : powerlevels.content.users) { if (user.second >= powerlevels.content.events_default && user.second >= powerlevels.content.state_default) { @@ -1525,12 +1541,13 @@ utils::roomVias(const std::string &roomid) }); // add the highest powerlevel user - auto max_pl_user = std::max_element( - users_with_high_pl_in_room.begin(), - users_with_high_pl_in_room.end(), - [&pl_content = powerlevels.content](const std::string &a, const std::string &b) { - return pl_content.user_level(a) < pl_content.user_level(b); - }); + auto max_pl_user = std::max_element(users_with_high_pl_in_room.begin(), + users_with_high_pl_in_room.end(), + [&pl_content = powerlevels.content, &create]( + const std::string &a, const std::string &b) { + return pl_content.user_level(a, create) < + pl_content.user_level(b, create); + }); if (max_pl_user != users_with_high_pl_in_room.end()) { auto host = mtx::identifiers::parse(*max_pl_user).hostname(); @@ -1705,11 +1722,15 @@ utils::updateSpaceVias() auto spaceid = roomid.toStdString(); + auto create = cache::client()->getStateEvent(spaceid).value_or( + mtx::events::StateEvent{}); + if (auto pl = cache::client() ->getStateEvent(spaceid) .value_or(mtx::events::StateEvent{}) .content; - pl.user_level(us) < pl.state_level(to_string(mtx::events::EventType::SpaceChild))) + pl.user_level(us, create) < + pl.state_level(to_string(mtx::events::EventType::SpaceChild))) continue; auto children = cache::client()->getChildRoomIds(spaceid); @@ -1748,12 +1769,16 @@ utils::updateSpaceVias() parent->origin_server_ts < weekAgo && // ignore unset spaces (parent->content.via && !parent->content.via->empty())) { + auto childCreate = + cache::client()->getStateEvent(spaceid).value_or( + mtx::events::StateEvent{}); + if (auto pl = cache::client() ->getStateEvent(childid) .value_or(mtx::events::StateEvent{}) .content; - pl.user_level(us) < + pl.user_level(us, childCreate) < pl.state_level(to_string(mtx::events::EventType::SpaceParent))) continue; @@ -2041,11 +2066,15 @@ utils::removeExpiredEvents() if (!asus->globalExpiry && !getExpEv(roomid)) continue; + auto create = cache::client()->getStateEvent(roomid).value_or( + mtx::events::StateEvent{}); + if (auto pl = cache::client() ->getStateEvent(roomid) .value_or(mtx::events::StateEvent{}) .content; - pl.user_level(us) < pl.event_level(to_string(mtx::events::EventType::RoomRedaction))) { + pl.user_level(us, create) < + pl.event_level(to_string(mtx::events::EventType::RoomRedaction))) { nhlog::net()->warn("Can't react events in {}, not running expiration.", roomid); continue; } diff --git a/src/timeline/Permissions.cpp b/src/timeline/Permissions.cpp index 569bf2bc..5e8bd169 100644 --- a/src/timeline/Permissions.cpp +++ b/src/timeline/Permissions.cpp @@ -4,6 +4,8 @@ #include "Permissions.h" +#include + #include "Cache_p.h" #include "MatrixClient.h" #include "TimelineModel.h" @@ -22,50 +24,53 @@ Permissions::invalidate() ->getStateEvent(roomId_.toStdString()) .value_or(mtx::events::StateEvent{}) .content; + create = cache::client() + ->getStateEvent(roomId_.toStdString()) + .value_or(mtx::events::StateEvent{}); } bool Permissions::canInvite() { - const bool plCheck = pl.user_level(http::client()->user_id().to_string()) >= pl.invite; - return plCheck || this->isV12Creator(); + const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.invite; + return plCheck; } bool Permissions::canBan() { - const bool plCheck = pl.user_level(http::client()->user_id().to_string()) >= pl.ban; - return plCheck || this->isV12Creator(); + const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.ban; + return plCheck; } bool Permissions::canKick() { - const bool plCheck = pl.user_level(http::client()->user_id().to_string()) >= pl.kick; - return plCheck || this->isV12Creator(); + const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.kick; + return plCheck; } bool Permissions::canRedact() { - const bool plCheck = pl.user_level(http::client()->user_id().to_string()) >= pl.redact; - return plCheck || this->isV12Creator(); + const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.redact; + return plCheck; } bool Permissions::canChange(int eventType) { - const bool plCheck = pl.user_level(http::client()->user_id().to_string()) >= + const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.state_level(to_string(qml_mtx_events::fromRoomEventType( static_cast(eventType)))); - return plCheck || this->isV12Creator(); + return plCheck; } bool Permissions::canSend(int eventType) { - const bool plCheck = pl.user_level(http::client()->user_id().to_string()) >= + const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.event_level(to_string(qml_mtx_events::fromRoomEventType( static_cast(eventType)))); - return plCheck || this->isV12Creator(); + return plCheck; } int @@ -94,15 +99,9 @@ Permissions::sendLevel(int eventType) bool Permissions::canPingRoom() { - const bool plCheck = pl.user_level(http::client()->user_id().to_string()) >= + const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.notification_level(mtx::events::state::notification_keys::room); - return plCheck || this->isV12Creator(); + return plCheck; } -bool -Permissions::isV12Creator() -{ - return cache::client()->isV12Creator(this->roomId_.toStdString(), - http::client()->user_id().to_string()); -} #include "moc_Permissions.cpp" diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h index ac36d5ea..9e2187a0 100644 --- a/src/timeline/Permissions.h +++ b/src/timeline/Permissions.h @@ -6,6 +6,8 @@ #include +#include +#include #include class TimelineModel; @@ -28,16 +30,20 @@ public: Q_INVOKABLE int redactLevel(); Q_INVOKABLE int changeLevel(int eventType); Q_INVOKABLE int sendLevel(int eventType); + Q_INVOKABLE qint64 creatorLevel() const { return mtx::events::state::Creator; } Q_INVOKABLE bool canPingRoom(); void invalidate(); const mtx::events::state::PowerLevels &powerlevelEvent() const { return pl; }; + const mtx::events::StateEvent &createEvent() const + { + return create; + }; private: - bool isV12Creator(); - QString roomId_; mtx::events::state::PowerLevels pl; + mtx::events::StateEvent create; }; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 7cb19c70..b00d7d8f 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -605,8 +605,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r case UserName: return QVariant(displayName(QString::fromStdString(acc::sender(event)))); case UserPowerlevel: { - return static_cast( - permissions_.powerlevelEvent().user_level(acc::sender(event))); + return static_cast(permissions_.powerlevelEvent().user_level( + acc::sender(event), permissions_.createEvent())); } case Day: { @@ -1413,12 +1413,6 @@ TimelineModel::readEvent(const std::string &id) !UserSettings::instance()->readReceipts()); } -bool -TimelineModel::isV12Creator(const QString &id) const -{ - return cache::isV12Creator(this->roomId().toStdString(), id.toStdString()); -} - QString TimelineModel::displayName(const QString &id) const { @@ -2427,6 +2421,7 @@ QString TimelineModel::formatPowerLevelEvent( const mtx::events::StateEvent &event) const { + const auto create = permissions_.createEvent(); mtx::events::StateEvent const *prevEvent = nullptr; if (!event.unsigned_data.replaces_state.empty()) { auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id); @@ -2446,15 +2441,15 @@ TimelineModel::formatPowerLevelEvent( if (!prevEvent) return tr("%1 has changed the room's permissions.").arg(sender_name); - auto calc_affected = [&event, - &prevEvent](int64_t newPowerlevelSetting) -> std::pair { + auto calc_affected = + [&event, &prevEvent, &create](int64_t newPowerlevelSetting) -> std::pair { QStringList affected{}; auto numberOfAffected = 0; // We do only compare to people with explicit PL. Usually others are not going to be // affected either way and this is cheaper to iterate over. for (auto const &[mxid, currentPowerlevel] : event.content.users) { if (currentPowerlevel == newPowerlevelSetting && - prevEvent->content.user_level(mxid) < newPowerlevelSetting) { + prevEvent->content.user_level(mxid, create) < newPowerlevelSetting) { numberOfAffected++; if (numberOfAffected <= 2) { affected.push_back(QString::fromStdString(mxid)); @@ -2631,24 +2626,25 @@ TimelineModel::formatPowerLevelEvent( // Compare if a Powerlevel of a user changed for (auto const &[mxid, powerlevel] : event.content.users) { auto nameOfChangedUser = utils::replaceEmoji(displayName(QString::fromStdString(mxid))); - if (prevEvent->content.user_level(mxid) != powerlevel) { + if (prevEvent->content.user_level(mxid, create) != powerlevel) { if (powerlevel >= administrator_power_level) { resultingMessage.append(tr("%1 has made %2 an administrator of this room.") .arg(sender_name, nameOfChangedUser)); } else if (powerlevel >= moderator_power_level && - powerlevel > prevEvent->content.user_level(mxid)) { + powerlevel > prevEvent->content.user_level(mxid, create)) { resultingMessage.append(tr("%1 has made %2 a moderator of this room.") .arg(sender_name, nameOfChangedUser)); } else if (powerlevel >= moderator_power_level && - powerlevel < prevEvent->content.user_level(mxid)) { + powerlevel < prevEvent->content.user_level(mxid, create)) { resultingMessage.append(tr("%1 has downgraded %2 to moderator of this room.") .arg(sender_name, nameOfChangedUser)); } else { - resultingMessage.append(tr("%1 has changed the powerlevel of %2 from %3 to %4.") - .arg(sender_name, - nameOfChangedUser, - QString::number(prevEvent->content.user_level(mxid)), - QString::number(powerlevel))); + resultingMessage.append( + tr("%1 has changed the powerlevel of %2 from %3 to %4.") + .arg(sender_name, + nameOfChangedUser, + QString::number(prevEvent->content.user_level(mxid, create)), + QString::number(powerlevel))); } } } @@ -3379,6 +3375,7 @@ TimelineModel::pushrulesRoomContext() const cache::displayName(room_id_.toStdString(), http::client()->user_id().to_string()), .member_count = cache::client()->memberCount(room_id_.toStdString()), .power_levels = permissions_.powerlevelEvent(), + .create = permissions_.createEvent(), }; } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index bae4533f..db7cee53 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -302,7 +302,6 @@ public: static QString getBareRoomLink(const QString &); static QString getRoomVias(const QString &); - Q_INVOKABLE bool isV12Creator(const QString &id) const; Q_INVOKABLE QString displayName(const QString &id) const; Q_INVOKABLE QString avatarUrl(const QString &id) const; Q_INVOKABLE QString formatDateSeparator(QDate date) const; From 014d70fd64b2eb0e9d5c3c0b6cb0815673db8c71 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 20 Feb 2026 17:45:00 +0100 Subject: [PATCH 3/9] Fix failed send indicator not updating automatically --- src/timeline/EventStore.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 52e7fb77..bc086a14 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -166,6 +166,11 @@ EventStore::EventStore(std::string room_id, QObject *) nhlog::ui()->debug("failing txn id '{}'", txn_id); cache::client()->removePendingStatus(room_id_, txn_id); current_txn_error_count = 0; + + auto idx = idToIndex(txn_id); + + if (idx) + emit dataChanged(*idx, *idx); } } QTimer::singleShot(1000, this, [this]() { From 51da48c706e28283bfb7f7ed4e70b984975e8b76 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 21 Feb 2026 00:43:33 +0100 Subject: [PATCH 4/9] Fix reply popup rendering on newer qt --- resources/qml/delegates/Reply.qml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 77cad0f0..0bc2e4b9 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -64,16 +64,17 @@ AbstractButton { height: r.limitHeight ? Math.min( timelineEvent.main?.height, timelineView.height / 10) + Nheko.paddingSmall + usernameBtn.height : undefined - // FIXME: I have no idea, why this name doesn't render in the reply popup on Qt 6.9.2 AbstractButton { id: usernameBtn - visible: r.eventId - contentItem: Label { - visible: r.eventId id: userName_ - text: r.userName + // HACK: To ensure the username gets rendered in Qt 6.9.2, + // we need to always have some text in here. The name + // should never be empty, since it falls to the mxid, but + // if we have no text there, Qt culls the item, before we + // fill it... + text: r.userName || "." color: r.userColor textFormat: Text.RichText width: timelineEvent.main?.width From 58e23302d0f00a31353aa60dcee7be5b1624107c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 21 Feb 2026 01:46:49 +0100 Subject: [PATCH 5/9] Register permissions as a qml type --- resources/qml/components/PowerlevelIndicator.qml | 2 +- src/timeline/Permissions.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/qml/components/PowerlevelIndicator.qml b/resources/qml/components/PowerlevelIndicator.qml index 00942aec..01f3bc39 100644 --- a/resources/qml/components/PowerlevelIndicator.qml +++ b/resources/qml/components/PowerlevelIndicator.qml @@ -8,7 +8,7 @@ import im.nheko Image { required property var powerlevel - required property var permissions + required property Permissions permissions readonly property bool isV12Creator: permissions ? permissions.creatorLevel() == powerlevel : false readonly property bool isAdmin: permissions ? permissions.changeLevel(MtxEvent.PowerLevels) <= powerlevel : false diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h index 9e2187a0..2829bedd 100644 --- a/src/timeline/Permissions.h +++ b/src/timeline/Permissions.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -15,6 +16,8 @@ class TimelineModel; class Permissions final : public QObject { Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("Only to be used to refer to C++ values") public: Permissions(QString roomId, QObject *parent = nullptr); From 5894e32482cc4c1e4b17533c4742435e830920a3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 22 Feb 2026 14:04:16 +0100 Subject: [PATCH 6/9] Fix rooms not getting marked as tombstoned on sync --- src/Cache.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Cache.cpp b/src/Cache.cpp index ee981abc..46034fe6 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -2479,11 +2479,12 @@ try { } } - updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); - updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); - updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); - updatedInfo.version = getRoomVersion(txn, statesdb).toStdString(); - updatedInfo.is_space = getRoomIsSpace(txn, statesdb); + updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); + updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); + updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); + updatedInfo.version = getRoomVersion(txn, statesdb).toStdString(); + updatedInfo.is_space = getRoomIsSpace(txn, statesdb); + updatedInfo.is_tombstoned = getRoomIsTombstoned(txn, statesdb); updatedInfo.notification_count = room.second.unread_notifications.notification_count; updatedInfo.highlight_count = room.second.unread_notifications.highlight_count; From b1c387e03c5bb58f86a303de6bd344655eafc8cb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 22 Feb 2026 17:17:43 +0100 Subject: [PATCH 7/9] Fix tombstones and create events getting mixed up --- src/Cache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cache.cpp b/src/Cache.cpp index 46034fe6..dac67ae4 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3624,7 +3624,7 @@ Cache::getRoomIsTombstoned(lmdb::txn &txn, lmdb::dbi &statesdb) using namespace mtx::events::state; std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event); + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomTombstone), event); if (res) { try { From 95532442cad439d2e05d634195c18a1a1727faf5 Mon Sep 17 00:00:00 2001 From: Florian Olk Date: Sun, 22 Feb 2026 19:32:53 +0100 Subject: [PATCH 8/9] fix: Remove 15fps limit for screensharing to prevent variable fps sources from crashing --- src/voip/CallDevices.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/voip/CallDevices.cpp b/src/voip/CallDevices.cpp index 90eef113..d42b378b 100644 --- a/src/voip/CallDevices.cpp +++ b/src/voip/CallDevices.cpp @@ -61,9 +61,7 @@ getFrameRate(const GValue *value) void addFrameRate(std::vector &rates, const FrameRate &rate) { - constexpr double minimumFrameRate = 15.0; - if (static_cast(rate.first) / rate.second >= minimumFrameRate) - rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second)); + rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second)); } void From 15322555e81a25fb6c360d20724da8c6c9226d5a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 23 Feb 2026 20:41:20 +0100 Subject: [PATCH 9/9] Limit forward completer height Fixes #2009 --- resources/qml/ForwardCompleter.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index c5c1689a..c79abbc2 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -57,6 +57,7 @@ Popup { eventId: mid userColor: TimelineManager.userColor(replyPreview.userId, palette.window) maxWidth: parent.width + limitHeight: true } MatrixTextField { id: roomTextInput