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<std::string>
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<mtx::events::EventType> &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<typename T>
+ std::optional<mtx::events::StateEvent<T>> getStateEvent(const std::string &room_id,
+ std::string_view state_key = "")
+ {
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ return getStateEvent<T>(txn, room_id, state_key);
+ }
+
//! Retrieve member info from a room.
std::vector<RoomMember> getMembers(const std::string &room_id,
std::size_t startIndex = 0,
@@ -406,7 +415,7 @@ private:
}
template<typename T>
- std::optional<mtx::events::StateEvent<T>> getStateEvent(lmdb::txn txn,
+ std::optional<mtx::events::StateEvent<T>> 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"(\[(.*?)(?<!\\)\]\((https://matrix.to/#/.*?\)))");
diff --git a/src/timeline/Permissions.cpp b/src/timeline/Permissions.cpp
new file mode 100644
index 00000000..1eaab468
--- /dev/null
+++ b/src/timeline/Permissions.cpp
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "Permissions.h"
+
+#include "Cache_p.h"
+#include "MatrixClient.h"
+#include "TimelineModel.h"
+
+Permissions::Permissions(TimelineModel *parent)
+ : QObject(parent)
+ , room(parent)
+{
+ invalidate();
+}
+
+void
+Permissions::invalidate()
+{
+ pl = cache::client()
+ ->getStateEvent<mtx::events::state::PowerLevels>(room->roomId().toStdString())
+ .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
+ .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<qml_mtx_events::EventType>(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<qml_mtx_events::EventType>(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 <QObject>
+
+#include <mtx/events/power_levels.hpp>
+
+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<int>(IsOnlyEmoji)));
m.insert(names[Body], data(event, static_cast<int>(Body)));
m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody)));
+ m.insert(names[IsSender], data(event, static_cast<int>(IsSender)));
m.insert(names[UserId], data(event, static_cast<int>(UserId)));
m.insert(names[UserName], data(event, static_cast<int>(UserName)));
m.insert(names[Day], data(event, static_cast<int>(Day)));
@@ -608,7 +717,10 @@ TimelineModel::syncState(const mtx::responses::State &s)
emit roomNameChanged();
else if (std::holds_alternative<StateEvent<state::Topic>>(e))
emit roomTopicChanged();
- else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
+ else if (std::holds_alternative<StateEvent<state::Topic>>(e)) {
+ permissions_.invalidate();
+ emit permissionsChanged();
+ } else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
emit roomAvatarUrlChanged();
emit roomNameChanged();
}
@@ -661,7 +773,10 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
emit roomNameChanged();
else if (std::holds_alternative<StateEvent<state::Topic>>(e))
emit roomTopicChanged();
- else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
+ else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) {
+ permissions_.invalidate();
+ emit permissionsChanged();
+ } else if (std::holds_alternative<StateEvent<state::Member>>(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 = "");
|