From ab0baf5d9eb3319f8d3da15bd966ab8ab2810fce Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 2 May 2021 18:01:18 +0200 Subject: [PATCH] Only show actions, when you have permissions to do them --- CMakeLists.txt | 2 + resources/qml/MessageInput.qml | 8 +++ resources/qml/MessageView.qml | 4 +- resources/qml/TimelineRow.qml | 4 +- resources/qml/TimelineView.qml | 9 ++- resources/qml/TopBar.qml | 1 + resources/qml/UserProfile.qml | 27 ++++---- src/Cache.h | 2 +- src/Cache_p.h | 11 ++- src/Config.h | 2 +- src/timeline/Permissions.cpp | 63 +++++++++++++++++ src/timeline/Permissions.h | 33 +++++++++ src/timeline/TimelineModel.cpp | 119 ++++++++++++++++++++++++++++++++- src/timeline/TimelineModel.h | 7 ++ src/ui/UserProfile.h | 2 + 15 files changed, 272 insertions(+), 22 deletions(-) create mode 100644 src/timeline/Permissions.cpp create mode 100644 src/timeline/Permissions.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b9304f01..4dd59b70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -271,6 +271,7 @@ set(SRC_FILES src/timeline/TimelineViewManager.cpp src/timeline/TimelineModel.cpp src/timeline/DelegateChooser.cpp + src/timeline/Permissions.cpp # UI components src/ui/Avatar.cpp @@ -494,6 +495,7 @@ qt5_wrap_cpp(MOC_HEADERS src/timeline/TimelineViewManager.h src/timeline/TimelineModel.h src/timeline/DelegateChooser.h + src/timeline/Permissions.h # UI components src/ui/Avatar.h diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 5bb699dd..c5dfbfa3 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -28,6 +28,7 @@ Rectangle { RowLayout { id: row + visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) || messageContextMenu.isSender anchors.fill: parent ImageButton { @@ -352,4 +353,11 @@ Rectangle { } + Text { + anchors.centerIn: parent + visible: TimelineManager.timeline ? (!TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage)) : false + text: qsTr("You don't have permission to send messages in this room") + color: colors.text + } + } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 1f483bf9..7dbe7e12 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -90,6 +90,7 @@ ScrollView { EmojiButton { id: reactButton + visible: chat.model ? chat.model.permissions.canSend(MtxEvent.Reaction) : false width: 16 hoverEnabled: true ToolTip.visible: hovered @@ -101,6 +102,7 @@ ScrollView { ImageButton { id: replyButton + visible: chat.model ? chat.model.permissions.canSend(MtxEvent.TextMessage) : false width: 16 hoverEnabled: true image: ":/icons/icons/ui/mail-reply.png" @@ -117,7 +119,7 @@ ScrollView { image: ":/icons/icons/ui/vertical-ellipsis.png" ToolTip.visible: hovered ToolTip.text: qsTr("Options") - onClicked: messageContextMenu.show(row.model.id, row.model.type, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton) + onClicked: messageContextMenu.show(row.model.id, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton) } } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 7bc3df63..715e8bd1 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -28,12 +28,12 @@ Item { TapHandler { acceptedButtons: Qt.RightButton - onSingleTapped: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) + onSingleTapped: messageContextMenu.show(model.id, model.type, model.isSender, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) gesturePolicy: TapHandler.ReleaseWithinBounds } TapHandler { - onLongPressed: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) + onLongPressed: messageContextMenu.show(model.id, model.type, model.isSender, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) onDoubleTapped: chat.model.reply = model.id gesturePolicy: TapHandler.ReleaseWithinBounds } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 36184015..6750b427 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -97,12 +97,14 @@ Page { property int eventType property bool isEncrypted property bool isEditable + property bool isSender - function show(eventId_, eventType_, isEncrypted_, isEditable_, link_, text_, showAt_) { + function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) { eventId = eventId_; eventType = eventType_; isEncrypted = isEncrypted_; isEditable = isEditable_; + isSender = isSender_; if (text_) text = text_; else @@ -134,6 +136,7 @@ Page { Platform.MenuItem { id: reactionOption + visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.Reaction) : false text: qsTr("React") onTriggered: emojiPopup.show(null, function(emoji) { TimelineManager.queueReactionMessage(messageContextMenu.eventId, emoji); @@ -141,12 +144,13 @@ Page { } Platform.MenuItem { + visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false text: qsTr("Reply") onTriggered: TimelineManager.timeline.replyAction(messageContextMenu.eventId) } Platform.MenuItem { - visible: messageContextMenu.isEditable + visible: messageContextMenu.isEditable && (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) enabled: visible text: qsTr("Edit") onTriggered: TimelineManager.timeline.editAction(messageContextMenu.eventId) @@ -185,6 +189,7 @@ Page { } Platform.MenuItem { + visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canRedact() : false) || messageContextMenu.isSender text: qsTr("Remove message") onTriggered: TimelineManager.timeline.redactEvent(messageContextMenu.eventId) } diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 858652c2..0b943ed1 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -101,6 +101,7 @@ Rectangle { id: roomOptionsMenu Platform.MenuItem { + visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canInvite() : false text: qsTr("Invite users") onTriggered: TimelineManager.openInviteUsersDialog() } diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml index d0d272c9..bd25b74e 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml @@ -44,7 +44,7 @@ ApplicationWindow { displayName: profile.displayName userid: profile.userid Layout.alignment: Qt.AlignHCenter - onClicked: profile.isSelf ? profile.changeAvatar() : TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id) + onClicked: profile.isSelf ? profile.changeAvatar() : TimelineManager.openImageOverlay(profile.avatarUrl, "") } BusyIndicator { @@ -151,18 +151,7 @@ ApplicationWindow { } RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 8 - - ImageButton { - image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png" - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Ban the user") - onClicked: profile.banUser() - } // ImageButton{ - // image:":/icons/icons/ui/volume-off-indicator.png" // Layout.margins: { // left: 5 @@ -174,6 +163,10 @@ ApplicationWindow { // profile.ignoreUser() // } // } + + Layout.alignment: Qt.AlignHCenter + spacing: 8 + ImageButton { image: ":/icons/icons/ui/black-bubble-speech.png" hoverEnabled: true @@ -188,6 +181,16 @@ ApplicationWindow { ToolTip.visible: hovered ToolTip.text: qsTr("Kick the user") onClicked: profile.kickUser() + visible: profile.room ? profile.room.permissions.canKick() : false + } + + ImageButton { + image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Ban the user") + onClicked: profile.banUser() + visible: profile.room ? profile.room.permissions.canBan() : false } } diff --git a/src/Cache.h b/src/Cache.h index e795b32a..427dbafc 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -126,7 +126,7 @@ getTimelineMentions(); std::vector roomMembers(const std::string &room_id); -//! Check if the given user has power leve greater than than +//! Check if the given user has power level greater than than //! lowest power level of the given events. bool hasEnoughPowerLevel(const std::vector &eventTypes, diff --git a/src/Cache_p.h b/src/Cache_p.h index 1e388e77..356c6e42 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -84,6 +84,15 @@ public: //! Retrieve the version of the room if any. QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Get a specific state event + template + std::optional> getStateEvent(const std::string &room_id, + std::string_view state_key = "") + { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + return getStateEvent(txn, room_id, state_key); + } + //! Retrieve member info from a room. std::vector getMembers(const std::string &room_id, std::size_t startIndex = 0, @@ -406,7 +415,7 @@ private: } template - std::optional> getStateEvent(lmdb::txn txn, + std::optional> getStateEvent(lmdb::txn &txn, const std::string &room_id, std::string_view state_key = "") { diff --git a/src/Config.h b/src/Config.h index 88452935..97669822 100644 --- a/src/Config.h +++ b/src/Config.h @@ -59,7 +59,7 @@ const QRegularExpression url_regex( // match an URL, that is not quoted, i.e. // vvvvvv match quote via negative lookahead/lookbehind vv // vvvv atomic match url -> fail if there is a " before or after vvv - R"(\b(?((www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s'"]+[^!,\.\s'"\]\)\:]))(?!["'])\b)"); + R"((?((www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'"]+[^!,\.\s<>'"\]\)\:]))(?!["']))"); // match any markdown matrix.to link. Capture group 1 is the link name, group 2 is the target. static const QRegularExpression matrixToMarkdownLink( R"(\[(.*?)(?getStateEvent(room->roomId().toStdString()) + .value_or(mtx::events::StateEvent{}) + .content; +} + +bool +Permissions::canInvite() +{ + return pl.user_level(http::client()->user_id().to_string()) >= pl.invite; +} + +bool +Permissions::canBan() +{ + return pl.user_level(http::client()->user_id().to_string()) >= pl.ban; +} + +bool +Permissions::canKick() +{ + return pl.user_level(http::client()->user_id().to_string()) >= pl.kick; +} + +bool +Permissions::canRedact() +{ + return pl.user_level(http::client()->user_id().to_string()) >= pl.redact; +} +bool +Permissions::canChange(int eventType) +{ + return pl.user_level(http::client()->user_id().to_string()) >= + pl.state_level(to_string(qml_mtx_events::fromRoomEventType( + static_cast(eventType)))); +} +bool +Permissions::canSend(int eventType) +{ + return pl.user_level(http::client()->user_id().to_string()) >= + pl.event_level(to_string(qml_mtx_events::fromRoomEventType( + static_cast(eventType)))); +} diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h new file mode 100644 index 00000000..f7e6f389 --- /dev/null +++ b/src/timeline/Permissions.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include + +class TimelineModel; + +class Permissions : public QObject +{ + Q_OBJECT + +public: + Permissions(TimelineModel *parent); + + Q_INVOKABLE bool canInvite(); + Q_INVOKABLE bool canBan(); + Q_INVOKABLE bool canKick(); + + Q_INVOKABLE bool canRedact(); + Q_INVOKABLE bool canChange(int eventType); + Q_INVOKABLE bool canSend(int eventType); + + void invalidate(); + +private: + TimelineModel *room; + mtx::events::state::PowerLevels pl; +}; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index c472ab33..a1e9ac0c 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -207,6 +207,111 @@ toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event) event); } +mtx::events::EventType +qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t) +{ + switch (t) { + // Unsupported event + case qml_mtx_events::Unsupported: + return mtx::events::EventType::Unsupported; + + /// m.room_key_request + case qml_mtx_events::KeyRequest: + return mtx::events::EventType::RoomKeyRequest; + /// m.reaction: + case qml_mtx_events::Reaction: + return mtx::events::EventType::Reaction; + /// m.room.aliases + case qml_mtx_events::Aliases: + return mtx::events::EventType::RoomAliases; + /// m.room.avatar + case qml_mtx_events::Avatar: + return mtx::events::EventType::RoomAvatar; + /// m.call.invite + case qml_mtx_events::CallInvite: + return mtx::events::EventType::CallInvite; + /// m.call.answer + case qml_mtx_events::CallAnswer: + return mtx::events::EventType::CallAnswer; + /// m.call.hangup + case qml_mtx_events::CallHangUp: + return mtx::events::EventType::CallHangUp; + /// m.call.candidates + case qml_mtx_events::CallCandidates: + return mtx::events::EventType::CallCandidates; + /// m.room.canonical_alias + case qml_mtx_events::CanonicalAlias: + return mtx::events::EventType::RoomCanonicalAlias; + /// m.room.create + case qml_mtx_events::RoomCreate: + return mtx::events::EventType::RoomCreate; + /// m.room.encrypted. + case qml_mtx_events::Encrypted: + return mtx::events::EventType::RoomEncrypted; + /// m.room.encryption. + case qml_mtx_events::Encryption: + return mtx::events::EventType::RoomEncryption; + /// m.room.guest_access + case qml_mtx_events::RoomGuestAccess: + return mtx::events::EventType::RoomGuestAccess; + /// m.room.history_visibility + case qml_mtx_events::RoomHistoryVisibility: + return mtx::events::EventType::RoomHistoryVisibility; + /// m.room.join_rules + case qml_mtx_events::RoomJoinRules: + return mtx::events::EventType::RoomJoinRules; + /// m.room.member + case qml_mtx_events::Member: + return mtx::events::EventType::RoomMember; + /// m.room.name + case qml_mtx_events::Name: + return mtx::events::EventType::RoomName; + /// m.room.power_levels + case qml_mtx_events::PowerLevels: + return mtx::events::EventType::RoomPowerLevels; + /// m.room.tombstone + case qml_mtx_events::Tombstone: + return mtx::events::EventType::RoomTombstone; + /// m.room.topic + case qml_mtx_events::Topic: + return mtx::events::EventType::RoomTopic; + /// m.room.redaction + case qml_mtx_events::Redaction: + return mtx::events::EventType::RoomRedaction; + /// m.room.pinned_events + case qml_mtx_events::PinnedEvents: + return mtx::events::EventType::RoomPinnedEvents; + // m.sticker + case qml_mtx_events::Sticker: + return mtx::events::EventType::Sticker; + // m.tag + case qml_mtx_events::Tag: + return mtx::events::EventType::Tag; + /// m.room.message + case qml_mtx_events::AudioMessage: + case qml_mtx_events::EmoteMessage: + case qml_mtx_events::FileMessage: + case qml_mtx_events::ImageMessage: + case qml_mtx_events::LocationMessage: + case qml_mtx_events::NoticeMessage: + case qml_mtx_events::TextMessage: + case qml_mtx_events::VideoMessage: + case qml_mtx_events::Redacted: + case qml_mtx_events::UnknownMessage: + case qml_mtx_events::KeyVerificationRequest: + case qml_mtx_events::KeyVerificationStart: + case qml_mtx_events::KeyVerificationMac: + case qml_mtx_events::KeyVerificationAccept: + case qml_mtx_events::KeyVerificationCancel: + case qml_mtx_events::KeyVerificationKey: + case qml_mtx_events::KeyVerificationDone: + case qml_mtx_events::KeyVerificationReady: + return mtx::events::EventType::RoomMessage; + default: + return mtx::events::EventType::Unsupported; + }; +} + TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) : QAbstractListModel(parent) , events(room_id.toStdString(), this) @@ -282,6 +387,7 @@ TimelineModel::roleNames() const {Body, "body"}, {FormattedBody, "formattedBody"}, {PreviousMessageUserId, "previousMessageUserId"}, + {IsSender, "isSender"}, {UserId, "userId"}, {UserName, "userName"}, {PreviousMessageDay, "previousMessageDay"}, @@ -333,6 +439,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r namespace acc = mtx::accessors; switch (role) { + case IsSender: + return QVariant(acc::sender(event) == http::client()->user_id().to_string()); case UserId: return QVariant(QString::fromStdString(acc::sender(event))); case UserName: @@ -497,6 +605,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r m.insert(names[IsOnlyEmoji], data(event, static_cast(IsOnlyEmoji))); m.insert(names[Body], data(event, static_cast(Body))); m.insert(names[FormattedBody], data(event, static_cast(FormattedBody))); + m.insert(names[IsSender], data(event, static_cast(IsSender))); m.insert(names[UserId], data(event, static_cast(UserId))); m.insert(names[UserName], data(event, static_cast(UserName))); m.insert(names[Day], data(event, static_cast(Day))); @@ -608,7 +717,10 @@ TimelineModel::syncState(const mtx::responses::State &s) emit roomNameChanged(); else if (std::holds_alternative>(e)) emit roomTopicChanged(); - else if (std::holds_alternative>(e)) { + else if (std::holds_alternative>(e)) { + permissions_.invalidate(); + emit permissionsChanged(); + } else if (std::holds_alternative>(e)) { emit roomAvatarUrlChanged(); emit roomNameChanged(); } @@ -661,7 +773,10 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) emit roomNameChanged(); else if (std::holds_alternative>(e)) emit roomTopicChanged(); - else if (std::holds_alternative>(e)) { + else if (std::holds_alternative>(e)) { + permissions_.invalidate(); + emit permissionsChanged(); + } else if (std::holds_alternative>(e)) { emit roomAvatarUrlChanged(); emit roomNameChanged(); } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 33e77f53..caeb25cf 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -16,6 +16,7 @@ #include "CacheCryptoStructs.h" #include "EventStore.h" #include "InputBar.h" +#include "Permissions.h" #include "ui/RoomSettings.h" #include "ui/UserProfile.h" @@ -105,6 +106,7 @@ enum EventType KeyVerificationReady }; Q_ENUM_NS(EventType) +mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType); enum EventState { @@ -159,6 +161,7 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged) Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) Q_PROPERTY(InputBar *input READ input CONSTANT) + Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged) public: explicit TimelineModel(TimelineViewManager *manager, @@ -173,6 +176,7 @@ public: Body, FormattedBody, PreviousMessageUserId, + IsSender, UserId, UserName, PreviousMessageDay, @@ -300,6 +304,7 @@ public slots: QString roomName() const; QString roomTopic() const; InputBar *input() { return &input_; } + Permissions *permissions() { return &permissions_; } QString roomAvatarUrl() const; QString roomId() const { return room_id_; } @@ -331,6 +336,7 @@ signals: void roomNameChanged(); void roomTopicChanged(); void roomAvatarUrlChanged(); + void permissionsChanged(); void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); void scrollTargetChanged(); @@ -359,6 +365,7 @@ private: TimelineViewManager *manager_; InputBar input_{this}; + Permissions permissions_{this}; QTimer showEventTimer{this}; QString eventIdToShow; diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h index 7c9c7495..aa7266ab 100644 --- a/src/ui/UserProfile.h +++ b/src/ui/UserProfile.h @@ -95,6 +95,7 @@ class UserProfile : public QObject Q_PROPERTY( bool userVerificationEnabled READ userVerificationEnabled NOTIFY userStatusChanged) Q_PROPERTY(bool isSelf READ isSelf CONSTANT) + Q_PROPERTY(TimelineModel *room READ room CONSTANT) public: UserProfile(QString roomid, QString userid, @@ -111,6 +112,7 @@ public: bool userVerificationEnabled() const; bool isSelf() const; bool isLoading() const; + TimelineModel *room() const { return model; } Q_INVOKABLE void verify(QString device = ""); Q_INVOKABLE void unverify(QString device = "");