Compare commits

...

12 commits

Author SHA1 Message Date
Nicolas Werner
15322555e8
Limit forward completer height
Fixes #2009
2026-02-23 20:43:24 +01:00
DeepBlueV7.X
b083dc801c
Merge pull request #2012 from TheTaktik/fix/1831-pipewire-screencast-crash
fix: Remove 15fps limit for screensharing sources
2026-02-23 00:09:06 +01:00
Florian Olk
95532442ca fix: Remove 15fps limit for screensharing to prevent variable fps sources from crashing 2026-02-22 19:36:05 +01:00
Nicolas Werner
b1c387e03c
Fix tombstones and create events getting mixed up 2026-02-22 17:17:43 +01:00
Nicolas Werner
5894e32482
Fix rooms not getting marked as tombstoned on sync 2026-02-22 14:04:16 +01:00
Nicolas Werner
58e23302d0
Register permissions as a qml type 2026-02-21 01:46:49 +01:00
Nicolas Werner
51da48c706
Fix reply popup rendering on newer qt 2026-02-21 00:43:33 +01:00
Nicolas Werner
014d70fd64
Fix failed send indicator not updating automatically 2026-02-20 17:45:00 +01:00
Nicolas Werner
f91427e653
Merge branch 'master' of github.com:Nheko-Reborn/nheko 2026-02-20 01:33:15 +01:00
Nicolas Werner
e3bc058845
Refactor v12 support to use new user_level helper from mtxclient 2026-02-20 01:32:37 +01:00
DeepBlueV7.X
abb2325a99
Merge pull request #2003 from Integral-Tech/fix-toggle-action
Update `toggleAction_` text when parent window visibility changes
2026-02-17 01:20:06 +01:00
Integral
b5ce330c82 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.
2026-02-12 14:38:49 +08:00
23 changed files with 237 additions and 230 deletions

View file

@ -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 "")

View file

@ -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

View file

@ -57,6 +57,7 @@ Popup {
eventId: mid
userColor: TimelineManager.userColor(replyPreview.userId, palette.window)
maxWidth: parent.width
limitHeight: true
}
MatrixTextField {
id: roomTextInput

View file

@ -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

View file

@ -7,10 +7,10 @@ import QtQuick.Controls
import im.nheko
Image {
required property int powerlevel
required property var permissions
required property bool isV12Creator
required property var powerlevel
required property Permissions permissions
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 {

View file

@ -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

View file

@ -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")

View file

@ -168,7 +168,6 @@ ApplicationWindow {
sourceSize.height: height
powerlevel: model.powerlevel
permissions: room.permissions
isV12Creator: room.isV12Creator(model.mxid)
}
EncryptionIndicator {

View file

@ -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;
@ -3623,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 {
@ -4621,12 +4622,14 @@ Cache::updateSpaces(lmdb::txn &txn,
event.state_key.at(0) == '!') {
const std::string &space = event.state_key;
auto create = getStateEvent<mtx::events::state::Create>(txn, space)
.value_or(mtx::events::StateEvent<mtx::events::state::Create>{});
auto pls = getStateEvent<mtx::events::state::PowerLevels>(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 +4638,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 +4854,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<Create> evt =
nlohmann::json::parse(create_event).get<StateEvent<Create>>();
if (evt.sender == user_id) {
return true;
}
const std::optional<std::vector<std::string>> &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<mtx::events::EventType> &eventTypes,
const std::string &room_id,
@ -4908,30 +4866,22 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &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<int64_t>::max();
int64_t user_level = std::numeric_limits<int64_t>::min();
std::string_view event;
bool res = db_.get(txn, to_string(EventType::RoomPowerLevels), event);
try {
StateEvent<Create> create = getStateEvent<mtx::events::state::Create>(txn, room_id)
.value_or(StateEvent<Create>{});
StateEvent<PowerLevels> pls =
getStateEvent<mtx::events::state::PowerLevels>(txn, room_id)
.value_or(StateEvent<PowerLevels>{});
if (res) {
try {
StateEvent<PowerLevels> msg =
nlohmann::json::parse(std::string_view(event.data(), event.size()))
.get<StateEvent<PowerLevels>>();
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 +6128,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 +6409,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)

View file

@ -123,10 +123,6 @@ runMigrations();
std::vector<std::string>
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

View file

@ -145,10 +145,6 @@ public:
//! Retrieve all the user ids from a room.
std::vector<std::string> 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<mtx::events::EventType> &eventTypes,

View file

@ -19,6 +19,9 @@ MemberListBackend::MemberListBackend(const QString &room_id, QObject *parent)
->getStateEvent<mtx::events::state::PowerLevels>(room_id_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content}
, create_{cache::client()
->getStateEvent<mtx::events::state::Create>(room_id_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::Create>{})}
{
try {
info_ = cache::singleRoomInfo(room_id_.toStdString());
@ -92,7 +95,7 @@ MemberListBackend::data(const QModelIndex &index, int role) const
}
case Powerlevel:
return static_cast<qlonglong>(
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"

View file

@ -73,6 +73,7 @@ private:
bool loadingMoreMembers_{false};
mtx::events::state::PowerLevels powerLevels_;
mtx::events::StateEvent<mtx::events::state::Create> 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;

View file

@ -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<mtx::events::state::Create> &create,
QObject *parent)
: QAbstractListModel(parent)
, room_id(rid)
, powerLevels_(pl)
, create_(create)
{
std::set<mtx::events::state::power_level_t> 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<mtx::events::state::Create> &create,
QObject *parent)
: QAbstractListModel(parent)
, room_id(rid)
, powerLevels_(pl)
, create_(create)
{
std::set<mtx::events::state::power_level_t> 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<std::string, mtx::events::state::power_level_t, std::less<>> 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<mtx::events::state::PowerLevels>(room_id.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content)
, types_(room_id.toStdString(), powerLevels_, this)
, users_(room_id.toStdString(), powerLevels_, this)
, spaces_(room_id.toStdString(), powerLevels_, this)
, create_(cache::client()
->getStateEvent<mtx::events::state::Create>(room_id.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::Create>{}))
, 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<mtx::events::state::Create> &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<std::string> visited;
@ -703,10 +735,16 @@ PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(const std::string &room_i
cache::client()->getStateEvent<mtx::events::state::space::Parent>(s, space);
if (parent && parent->content.via && !parent->content.via->empty() &&
parent->content.canonical) {
auto parentPl = cache::client()->getStateEvent<mtx::events::state::PowerLevels>(s);
auto childPl = cache::client()->getStateEvent<mtx::events::state::PowerLevels>(s);
auto childCreate =
cache::client()->getStateEvent<mtx::events::state::Create>(s).value_or(
mtx::events::StateEvent<mtx::events::state::Create>{});
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_);

View file

@ -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<mtx::events::state::Create> &create,
QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast<int>(types.size()); }
@ -67,6 +69,7 @@ public:
std::string room_id;
QVector<Entry> types;
mtx::events::state::PowerLevels powerLevels_;
mtx::events::StateEvent<mtx::events::state::Create> 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<mtx::events::state::Create> &create,
QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast<int>(users.size()); }
@ -121,6 +126,7 @@ public:
std::string room_id;
QVector<Entry> users;
mtx::events::state::PowerLevels powerLevels_;
mtx::events::StateEvent<mtx::events::state::Create> 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<mtx::events::state::Create> &create,
QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast<int>(spaces.size()); }
@ -183,6 +191,7 @@ public:
std::string roomid;
mtx::events::state::PowerLevels pl;
mtx::events::StateEvent<mtx::events::state::Create> 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<mtx::events::state::Create> create_;
PowerlevelsTypeListModel types_;
PowerlevelsUserListModel users_;
PowerlevelsSpacesListModel spaces_;

View file

@ -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);

View file

@ -1453,6 +1453,9 @@ utils::roomVias(const std::string &roomid)
auto powerlevels =
cache::client()->getStateEvent<mtx::events::state::PowerLevels>(roomid).value_or(
mtx::events::StateEvent<mtx::events::state::PowerLevels>{});
auto create =
cache::client()->getStateEvent<mtx::events::state::Create>(roomid).value_or(
mtx::events::StateEvent<mtx::events::state::Create>{});
auto acls = cache::client()->getStateEvent<mtx::events::state::ServerAcl>(roomid);
std::vector<QRegularExpression> allowedServers;
@ -1501,6 +1504,19 @@ utils::roomVias(const std::string &roomid)
std::set<std::string> 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<mtx::identifiers::User>(user).hostname();
if (isHostAllowed(host))
users_with_high_pl.insert(user);
}
for (const auto &user : create.content.additional_creators) {
auto host = mtx::identifiers::parse<mtx::identifiers::User>(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<mtx::identifiers::User>(*max_pl_user).hostname();
@ -1705,11 +1722,15 @@ utils::updateSpaceVias()
auto spaceid = roomid.toStdString();
auto create = cache::client()->getStateEvent<mtx::events::state::Create>(spaceid).value_or(
mtx::events::StateEvent<mtx::events::state::Create>{});
if (auto pl = cache::client()
->getStateEvent<mtx::events::state::PowerLevels>(spaceid)
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.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<mtx::events::state::Create>(spaceid).value_or(
mtx::events::StateEvent<mtx::events::state::Create>{});
if (auto pl =
cache::client()
->getStateEvent<mtx::events::state::PowerLevels>(childid)
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.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<mtx::events::state::Create>(roomid).value_or(
mtx::events::StateEvent<mtx::events::state::Create>{});
if (auto pl = cache::client()
->getStateEvent<mtx::events::state::PowerLevels>(roomid)
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.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;
}

View file

@ -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]() {

View file

@ -4,6 +4,8 @@
#include "Permissions.h"
#include <algorithm>
#include "Cache_p.h"
#include "MatrixClient.h"
#include "TimelineModel.h"
@ -22,50 +24,53 @@ Permissions::invalidate()
->getStateEvent<mtx::events::state::PowerLevels>(roomId_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content;
create = cache::client()
->getStateEvent<mtx::events::state::Create>(roomId_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::Create>{});
}
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<qml_mtx_events::EventType>(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<qml_mtx_events::EventType>(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"

View file

@ -5,7 +5,10 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <mtx/events.hpp>
#include <mtx/events/create.hpp>
#include <mtx/events/power_levels.hpp>
class TimelineModel;
@ -13,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);
@ -28,16 +33,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<mtx::events::state::Create> &createEvent() const
{
return create;
};
private:
bool isV12Creator();
QString roomId_;
mtx::events::state::PowerLevels pl;
mtx::events::StateEvent<mtx::events::state::Create> create;
};

View file

@ -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<qlonglong>(
permissions_.powerlevelEvent().user_level(acc::sender(event)));
return static_cast<qlonglong>(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<mtx::events::state::PowerLevels> &event) const
{
const auto create = permissions_.createEvent();
mtx::events::StateEvent<mtx::events::state::PowerLevels> 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<QStringList, int> {
auto calc_affected =
[&event, &prevEvent, &create](int64_t newPowerlevelSetting) -> std::pair<QStringList, int> {
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(),
};
}

View file

@ -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;

View file

@ -61,9 +61,7 @@ getFrameRate(const GValue *value)
void
addFrameRate(std::vector<std::string> &rates, const FrameRate &rate)
{
constexpr double minimumFrameRate = 15.0;
if (static_cast<double>(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