From 6c57fa6c5b491e981958e417458edac40e9000b4 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 10 Jun 2021 20:11:49 -0400 Subject: QML the invite dialog This also adds a property `roomId` to TimelineModel. --- src/ChatPage.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'src/ChatPage.cpp') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 10a91557..f6ea4539 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -140,6 +140,34 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) } }); + connect( + view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList); + connect( + view_manager_, + &TimelineViewManager::inviteUsers, + this, + [this](QString roomId, QStringList users) { + for (int ii = 0; ii < users.size(); ++ii) { + QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() { + const auto user = users.at(ii); + + http::client()->invite_user( + roomId.toStdString(), + user.toStdString(), + [this, user](const mtx::responses::RoomInvite &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to invite user: %1").arg(user)); + return; + } + + emit showNotification(tr("Invited user: %1").arg(user)); + }); + }); + } + }); + connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection); connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications); -- cgit 1.5.1 From e1acf5d324615e8c61c469883a6a42933c8f76bc Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 10 Jun 2021 20:13:12 -0400 Subject: make lint --- CMakeLists.txt | 2 + resources/qml/InviteDialog.qml | 78 +++++++++++++++++++++++------------- resources/qml/RoomMembers.qml | 9 +++++ resources/qml/Root.qml | 38 ++++++++++++++++++ resources/qml/TimelineView.qml | 1 - resources/qml/TopBar.qml | 14 +------ resources/qml/types/Invitee.qml | 5 --- resources/res.qrc | 1 - src/ChatPage.cpp | 49 +++++++++++----------- src/InviteesModel.cpp | 77 +++++++++++++++++++++++++++++++++++ src/InviteesModel.h | 56 ++++++++++++++++++++++++++ src/MemberList.cpp | 10 +++-- src/timeline/TimelineModel.cpp | 10 +++++ src/timeline/TimelineModel.h | 3 ++ src/timeline/TimelineViewManager.cpp | 70 +++++++------------------------- 15 files changed, 290 insertions(+), 133 deletions(-) delete mode 100644 resources/qml/types/Invitee.qml create mode 100644 src/InviteesModel.cpp create mode 100644 src/InviteesModel.h (limited to 'src/ChatPage.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index 56592950..f77d9978 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -344,6 +344,7 @@ set(SRC_FILES src/CompletionProxyModel.cpp src/DeviceVerificationFlow.cpp src/EventAccessors.cpp + src/InviteesModel.cpp src/Logging.cpp src/LoginPage.cpp src/MainWindow.cpp @@ -550,6 +551,7 @@ qt5_wrap_cpp(MOC_HEADERS src/Clipboard.h src/CompletionProxyModel.h src/DeviceVerificationFlow.h + src/InviteesModel.h src/LoginPage.h src/MainWindow.h src/MemberList.h diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index 5d3a8f1e..d5cc4c6d 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -2,50 +2,28 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import im.nheko 1.0 -import "./types" ApplicationWindow { id: inviteDialogRoot property string roomId property string roomName - property list invitees + property InviteesModel invitees function addInvite() { if (inviteeEntry.text.match("@.+?:.{3,}")) { - invitees.push(inviteeComponent.createObject( - inviteDialogRoot, { - "invitee": inviteeEntry.text - })); + invitees.addUser(inviteeEntry.text); inviteeEntry.clear(); } } - function accept() { - if (inviteeEntry.text !== "") - addInvite(); - - var inviteeStringList = ["temp"]; // the "temp" element exists to declare this as a string array - for (var i = 0; i < invitees.length; ++i) - inviteeStringList.push(invitees[i].invitee); - inviteeStringList.shift(); // remove the first item - - TimelineManager.inviteUsers(inviteDialogRoot.roomId, inviteeStringList); - } - title: qsTr("Invite users to ") + roomName x: MainWindow.x + (MainWindow.width / 2) - (width / 2) y: MainWindow.y + (MainWindow.height / 2) - (height / 2) height: 380 width: 340 - Component { - id: inviteeComponent - - Invitee {} - } - // TODO: make this work in the TextField Shortcut { sequence: "Ctrl+Enter" @@ -74,7 +52,7 @@ ApplicationWindow { } Button { - text: qsTr("Invite") + text: qsTr("Add") onClicked: if (inviteeEntry.text !== "") addInvite() } } @@ -85,9 +63,53 @@ ApplicationWindow { Layout.fillWidth: true Layout.fillHeight: true model: invitees - delegate: Label { - text: model.invitee + + delegate: RowLayout { + spacing: 10 + + Avatar { + width: avatarSize + height: avatarSize + userid: model.mxid + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.displayName + onClicked: TimelineManager.timeline.openUserProfile(model.mxid) + } + + ColumnLayout { + spacing: 5 + + Label { + text: model.displayName + color: TimelineManager.userColor(model ? model.mxid : "", colors.window) + font.pointSize: 12 + } + + Label { + text: model.mxid + color: colors.buttonText + font.pointSize: 10 + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + } } +// delegate: RowLayout { +// spacing: 10 + +// Avatar { +// url: model.avatarUrl +// width: 20 +// height: width +// } + +// Label { +// text: model.displayName + " (" + model.mxid + ")" +// } +// } } } @@ -98,7 +120,7 @@ ApplicationWindow { text: qsTr("Invite") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole onClicked: { - inviteDialogRoot.accept(); + invitees.accept(); inviteDialogRoot.close(); } } diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml index 4406c1b0..d31fe319 100644 --- a/resources/qml/RoomMembers.qml +++ b/resources/qml/RoomMembers.qml @@ -44,6 +44,15 @@ ApplicationWindow { Layout.alignment: Qt.AlignHCenter } + ImageButton { + Layout.alignment: Qt.AlignHCenter + image: ":/icons/icons/ui/add-square-button.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Invite more people") + onClicked: TimelineManager.timeline.openInviteUsersDialog() + } + ScrollView { clip: false palette: colors diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 5316e20d..ecd0bdb7 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -89,6 +89,12 @@ Page { } } + Component { + id: inviteDialog + + InviteDialog { + } + } Connections { target: TimelineManager @@ -116,6 +122,38 @@ Page { } } + Connections { + target: TimelineManager.timeline + onOpenRoomMembersDialog: { + var membersDialog = roomMembersComponent.createObject(timelineRoot, { + "members": members, + "roomName": TimelineManager.timeline.roomName + }); + membersDialog.show(); + } + } + + Connections { + target: TimelineManager.timeline + onOpenRoomSettingsDialog: { + var roomSettings = roomSettingsComponent.createObject(timelineRoot, { + "roomSettings": settings + }); + roomSettings.show(); + } + } + + Connections { + target: TimelineManager.timeline + onOpenInviteUsersDialog: { + var dialog = inviteDialog.createObject(timelineRoot, { + "roomId": TimelineManager.timeline.roomId, + "roomName": TimelineManager.timeline.roomName + }); + dialog.show(); + } + } + ChatPage { anchors.fill: parent } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 148a5817..d515b9b4 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -157,7 +157,6 @@ Item { Layout.alignment: Qt.AlignHCenter enabled: false } - MatrixText { text: parent.roomName font.pixelSize: 24 diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 72dbe604..6cf747c5 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -21,12 +21,6 @@ Rectangle { z: 3 color: Nheko.colors.window - Component { - id: inviteDialog - - InviteDialog {} - } - TapHandler { onSingleTapped: { if (room) @@ -117,13 +111,7 @@ Rectangle { Platform.MenuItem { visible: room ? room.permissions.canInvite() : false text: qsTr("Invite users") - onTriggered: { - var dialog = inviteDialog.createObject(topBar, { - "roomId": room.roomId, - "roomName": room.roomName - }); - dialog.show(); - } + onTriggered: TimelineManager.timeline.openInviteUsers() } Platform.MenuItem { diff --git a/resources/qml/types/Invitee.qml b/resources/qml/types/Invitee.qml deleted file mode 100644 index fbc0b781..00000000 --- a/resources/qml/types/Invitee.qml +++ /dev/null @@ -1,5 +0,0 @@ -import QtQuick 2.12 - -Item { - property string invitee -} diff --git a/resources/res.qrc b/resources/res.qrc index ad7b6665..f8c040e4 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -176,7 +176,6 @@ qml/components/FlatButton.qml qml/RoomMembers.qml qml/InviteDialog.qml - qml/types/Invitee.qml media/ring.ogg diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index f6ea4539..8b4cfeef 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -116,32 +116,31 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); - connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) { - const auto room_id = currentRoom().toStdString(); - - for (int ii = 0; ii < users.size(); ++ii) { - QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { - const auto user = users.at(ii); - - http::client()->invite_user( - room_id, - user.toStdString(), - [this, user](const mtx::responses::RoomInvite &, - mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to invite user: %1").arg(user)); - return; - } + // TODO: once this signal is moved, reenable this +// connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) { +// const auto room_id = currentRoom().toStdString(); + +// for (int ii = 0; ii < users.size(); ++ii) { +// QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { +// const auto user = users.at(ii); + +// http::client()->invite_user( +// room_id, +// user.toStdString(), +// [this, user](const mtx::responses::RoomInvite &, +// mtx::http::RequestErr err) { +// if (err) { +// emit showNotification( +// tr("Failed to invite user: %1").arg(user)); +// return; +// } + +// emit showNotification(tr("Invited user: %1").arg(user)); +// }); +// }); +// } +// }); - emit showNotification(tr("Invited user: %1").arg(user)); - }); - }); - } - }); - - connect( - view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList); connect( view_manager_, &TimelineViewManager::inviteUsers, diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp new file mode 100644 index 00000000..849c5281 --- /dev/null +++ b/src/InviteesModel.cpp @@ -0,0 +1,77 @@ +#include "InviteesModel.h" + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" +#include "mtx/responses/profile.hpp" + +InviteesModel::InviteesModel(QObject *parent) + : QAbstractListModel{parent} +{} + +void +InviteesModel::addUser(QString mxid) +{ + beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count()); + + auto invitee = new Invitee{mxid, this}; + connect(invitee, &Invitee::userInfoLoaded, this, [this]() { + emit dataChanged(QModelIndex{}, QModelIndex{}); + }); + + invitees_.push_back(invitee); + + endInsertRows(); +} + +QHash +InviteesModel::roleNames() const +{ + return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}}; +} + +QVariant +InviteesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0) + return {}; + + switch (role) { + case Mxid: + return invitees_[index.row()]->mxid_; + case DisplayName: + return invitees_[index.row()]->displayName_; + case AvatarUrl: + return invitees_[index.row()]->avatarUrl_; + default: + return {}; + } +} + +QStringList +InviteesModel::mxids() +{ + QStringList mxidList; + for (int i = 0; i < invitees_.length(); ++i) + mxidList.push_back(invitees_[i]->mxid_); + return mxidList; +} + +Invitee::Invitee(const QString &mxid, QObject *parent) + : QObject{parent} + , mxid_{mxid} +{ + http::client()->get_profile( + mxid_.toStdString(), + [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve own profile info"); + return; + } + + displayName_ = QString::fromStdString(res.display_name); + avatarUrl_ = QString::fromStdString(res.avatar_url); + + emit userInfoLoaded(); + }); +} diff --git a/src/InviteesModel.h b/src/InviteesModel.h new file mode 100644 index 00000000..4bcc4e9d --- /dev/null +++ b/src/InviteesModel.h @@ -0,0 +1,56 @@ +#ifndef INVITEESMODEL_H +#define INVITEESMODEL_H + +#include +#include + +class Invitee : public QObject +{ + Q_OBJECT + +public: + Invitee(const QString &mxid, QObject *parent = nullptr); + +signals: + void userInfoLoaded(); + +private: + const QString mxid_; + QString displayName_; + QString avatarUrl_; + + friend class InviteesModel; +}; + +class InviteesModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + }; + + InviteesModel(QObject *parent = nullptr); + + Q_INVOKABLE void addUser(QString mxid); + + QHash roleNames() const override; + int rowCount(const QModelIndex & = QModelIndex()) const override + { + return (int)invitees_.size(); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QStringList mxids(); + +signals: + void accept(); + +private: + QVector invitees_; +}; + +#endif // INVITEESMODEL_H diff --git a/src/MemberList.cpp b/src/MemberList.cpp index 62488277..2a9c3fbc 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -43,7 +43,8 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) void MemberList::addUsers(const std::vector &members) { - beginInsertRows(QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); + beginInsertRows( + QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); for (const auto &member : members) m_memberList.push_back( @@ -78,10 +79,11 @@ MemberList::data(const QModelIndex &index, int role) const } } -bool MemberList::canFetchMore(const QModelIndex &) const +bool +MemberList::canFetchMore(const QModelIndex &) const { - const size_t numMembers = rowCount(); - return (numMembers > 1 && numMembers < info_.member_count); + const size_t numMembers = rowCount(); + return (numMembers > 1 && numMembers < info_.member_count); } void diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 48d69493..2127801c 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1076,6 +1076,16 @@ TimelineModel::openRoomSettings(QString room_id) openRoomSettingsDialog(settings); } +void +TimelineModel::openInviteUsers(QString room_id) +{ + InviteesModel *model = new InviteesModel{this}; + connect(model, &InviteesModel::accept, this, [this, model, room_id]() { + manager_->inviteUsers(room_id, model->mxids()); + }); + openInviteUsersDialog(model); +} + void TimelineModel::replyAction(QString id) { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 5730fbab..e5189e61 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -17,6 +17,7 @@ #include "CacheStructs.h" #include "EventStore.h" #include "InputBar.h" +#include "InviteesModel.h" #include "MemberList.h" #include "Permissions.h" #include "ui/RoomSettings.h" @@ -239,6 +240,7 @@ public: Q_INVOKABLE void openUserProfile(QString userid); Q_INVOKABLE void openRoomMembers(); Q_INVOKABLE void openRoomSettings(QString room_id = QString()); + Q_INVOKABLE void openInviteUsers(QString room_id = QString()); Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); Q_INVOKABLE void readReceiptsAction(QString id) const; @@ -357,6 +359,7 @@ signals: void openRoomMembersDialog(MemberList *members); void openRoomSettingsDialog(RoomSettings *settings); + void openInviteUsersDialog(InviteesModel *invitees); void newMessageToSend(mtx::events::collections::TimelineEvents event); void addPendingMessageToStore(mtx::events::collections::TimelineEvents event); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 43b9a646..08b88efd 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -20,6 +20,7 @@ #include "DeviceVerificationFlow.h" #include "EventAccessors.h" #include "ImagePackModel.h" +#include "InviteesModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -184,6 +185,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "Room Settings needs to be instantiated on the C++ side"); qmlRegisterUncreatableType( "im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "InviteesModel", + "InviteesModel needs to be instantiated on the C++ side"); static auto self = this; qmlRegisterSingletonType( @@ -423,62 +430,13 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) }); } -void -TimelineViewManager::openInviteUsersDialog() -{ - MainWindow::instance()->openInviteUsersDialog( - [this](const QStringList &invitees) { emit inviteUsers(invitees); }); -} - -void -TimelineViewManager::openLink(QString link) const -{ - QUrl url(link); - if (url.scheme() == "https" && url.host() == "matrix.to") { - // handle matrix.to links internally - QString p = url.fragment(QUrl::FullyEncoded); - if (p.startsWith("/")) - p.remove(0, 1); - - auto temp = p.split("?"); - QString query; - if (temp.size() >= 2) - query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); - - temp = temp.first().split("/"); - auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); - QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); - if (!identifier.isEmpty()) { - if (identifier.startsWith("@")) { - QByteArray uri = - "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } else if (identifier.startsWith("#")) { - QByteArray uri = - "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - uri.append("/e/" + - QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } else if (identifier.startsWith("!")) { - QByteArray uri = "matrix:roomid/" + - QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - uri.append("/e/" + - QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } - } - } else { - QDesktopServices::openUrl(url); - } -} +//void +//TimelineViewManager::openInviteUsersDialog() +//{ + // TODO: move this somewhere where it will actually work (probably Rooms) +// MainWindow::instance()->openInviteUsersDialog( +// [this](const QStringList &invitees) { emit inviteUsers(invitees); }); +//} void TimelineViewManager::openLeaveRoomDialog(QString roomid) const -- cgit 1.5.1 From d2d5229ede5124ba6cf9e85790dcd564faad00db Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 17 Jul 2021 13:31:38 -0400 Subject: make lint --- resources/qml/InviteDialog.qml | 44 ++++++++++++++++++++++++-------- resources/qml/RoomMembers.qml | 4 +++ resources/qml/TimelineView.qml | 1 + src/ChatPage.cpp | 49 +++++++++++++++++++----------------- src/InviteesModel.cpp | 4 +++ src/InviteesModel.h | 4 +++ src/timeline/TimelineViewManager.cpp | 6 ++--- 7 files changed, 75 insertions(+), 37 deletions(-) (limited to 'src/ChatPage.cpp') diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index 2932e398..ae74d3da 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 @@ -11,14 +15,11 @@ ApplicationWindow { property InviteesModel invitees function addInvite() { - if (inviteeEntry.text.match("@.+?:.{3,}")) - { + if (inviteeEntry.text.match("@.+?:.{3,}")) { invitees.addUser(inviteeEntry.text); inviteeEntry.clear(); - } - else - { - warningLabel.show() + } else { + warningLabel.show(); } } @@ -57,19 +58,29 @@ ApplicationWindow { placeholderText: qsTr("@joe:matrix.org", "Example user id. The name 'joe' can be localized however you want.") Layout.fillWidth: true - onAccepted: if (text !== "") addInvite() + onAccepted: { + if (text !== "") { + addInvite(); + } + } Component.onCompleted: forceActiveFocus() Shortcut { sequence: "Ctrl+Enter" onActivated: invitees.accept() } + } Button { text: qsTr("Add") - onClicked: if (inviteeEntry.text !== "") addInvite() + onClicked: { + if (inviteeEntry.text !== "") { + addInvite(); + } + } } + } Label { @@ -85,26 +96,28 @@ ApplicationWindow { visible: false opacity: 0 state: "hidden" - states: [ State { name: "shown" + PropertyChanges { target: warningLabel opacity: 1 visible: true } + }, State { name: "hidden" + PropertyChanges { target: warningLabel opacity: 0 visible: false } + } ] - transitions: [ Transition { from: "shown" @@ -122,7 +135,9 @@ ApplicationWindow { target: warningLabel property: "visible" } + } + } ] @@ -134,6 +149,7 @@ ApplicationWindow { running: false onTriggered: warningLabel.state = "hidden" } + } ListView { @@ -174,9 +190,13 @@ ApplicationWindow { Layout.fillHeight: true Layout.fillWidth: true } + } + } + } + } footer: DialogButtonBox { @@ -194,7 +214,9 @@ ApplicationWindow { Button { text: qsTr("Cancel") DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole - onClicked: inviteDialogRoot.close(); + onClicked: inviteDialogRoot.close() } + } + } diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml index 8addd704..44b917b1 100644 --- a/resources/qml/RoomMembers.qml +++ b/resources/qml/RoomMembers.qml @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index d515b9b4..148a5817 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -157,6 +157,7 @@ Item { Layout.alignment: Qt.AlignHCenter enabled: false } + MatrixText { text: parent.roomName font.pixelSize: 24 diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 8b4cfeef..70fd32fd 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -117,29 +117,32 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); // TODO: once this signal is moved, reenable this -// connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) { -// const auto room_id = currentRoom().toStdString(); - -// for (int ii = 0; ii < users.size(); ++ii) { -// QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { -// const auto user = users.at(ii); - -// http::client()->invite_user( -// room_id, -// user.toStdString(), -// [this, user](const mtx::responses::RoomInvite &, -// mtx::http::RequestErr err) { -// if (err) { -// emit showNotification( -// tr("Failed to invite user: %1").arg(user)); -// return; -// } - -// emit showNotification(tr("Invited user: %1").arg(user)); -// }); -// }); -// } -// }); + // connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList + // users) { + // const auto room_id = currentRoom().toStdString(); + + // for (int ii = 0; ii < users.size(); ++ii) { + // QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { + // const auto user = users.at(ii); + + // http::client()->invite_user( + // room_id, + // user.toStdString(), + // [this, user](const mtx::responses::RoomInvite &, + // mtx::http::RequestErr err) { + // if (err) { + // emit showNotification( + // tr("Failed to invite user: + // %1").arg(user)); + // return; + // } + + // emit showNotification(tr("Invited user: + // %1").arg(user)); + // }); + // }); + // } + // }); connect( view_manager_, diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index 1da7baf4..59054690 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + #include "InviteesModel.h" #include "Cache.h" diff --git a/src/InviteesModel.h b/src/InviteesModel.h index 4bcc4e9d..ac9208a0 100644 --- a/src/InviteesModel.h +++ b/src/InviteesModel.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + #ifndef INVITEESMODEL_H #define INVITEESMODEL_H diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 08b88efd..8daa2124 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -430,10 +430,10 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) }); } -//void -//TimelineViewManager::openInviteUsersDialog() +// void +// TimelineViewManager::openInviteUsersDialog() //{ - // TODO: move this somewhere where it will actually work (probably Rooms) +// TODO: move this somewhere where it will actually work (probably Rooms) // MainWindow::instance()->openInviteUsersDialog( // [this](const QStringList &invitees) { emit inviteUsers(invitees); }); //} -- cgit 1.5.1 From baa9dfe110698a741eedc6209e33a4db687dffbe Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 17 Jul 2021 16:49:34 -0400 Subject: Clean up code --- src/ChatPage.cpp | 28 ---------------------------- src/timeline/TimelineModel.cpp | 6 +++--- src/timeline/TimelineViewManager.cpp | 8 -------- 3 files changed, 3 insertions(+), 39 deletions(-) (limited to 'src/ChatPage.cpp') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 70fd32fd..6b8c1e10 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -116,34 +116,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); - // TODO: once this signal is moved, reenable this - // connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList - // users) { - // const auto room_id = currentRoom().toStdString(); - - // for (int ii = 0; ii < users.size(); ++ii) { - // QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { - // const auto user = users.at(ii); - - // http::client()->invite_user( - // room_id, - // user.toStdString(), - // [this, user](const mtx::responses::RoomInvite &, - // mtx::http::RequestErr err) { - // if (err) { - // emit showNotification( - // tr("Failed to invite user: - // %1").arg(user)); - // return; - // } - - // emit showNotification(tr("Invited user: - // %1").arg(user)); - // }); - // }); - // } - // }); - connect( view_manager_, &TimelineViewManager::inviteUsers, diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 516a499b..7ce0e98a 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1074,7 +1074,7 @@ TimelineModel::openRoomSettings(QString room_id) { RoomSettings *settings = new RoomSettings(room_id == QString() ? roomId() : room_id, this); connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged); - openRoomSettingsDialog(settings); + emit openRoomSettingsDialog(settings); } void @@ -1082,9 +1082,9 @@ TimelineModel::openInviteUsers(QString roomId) { InviteesModel *model = new InviteesModel{this}; connect(model, &InviteesModel::accept, this, [this, model, roomId]() { - manager_->inviteUsers(roomId == QString() ? room_id_ : roomId, model->mxids()); + emit manager_->inviteUsers(roomId == QString() ? room_id_ : roomId, model->mxids()); }); - openInviteUsersDialog(model); + emit openInviteUsersDialog(model); } void diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 8daa2124..64493e5b 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -430,14 +430,6 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) }); } -// void -// TimelineViewManager::openInviteUsersDialog() -//{ -// TODO: move this somewhere where it will actually work (probably Rooms) -// MainWindow::instance()->openInviteUsersDialog( -// [this](const QStringList &invitees) { emit inviteUsers(invitees); }); -//} - void TimelineViewManager::openLeaveRoomDialog(QString roomid) const { -- cgit 1.5.1 From a61678242b91944c07bf4b82b4eb25baad0db00b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 24 Jul 2021 14:59:14 +0200 Subject: Fix edge case that could lead to no new one time keys being uploaded --- src/ChatPage.cpp | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) (limited to 'src/ChatPage.cpp') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 6b8c1e10..615a15b3 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -929,31 +929,33 @@ ChatPage::currentPresence() const void ChatPage::ensureOneTimeKeyCount(const std::map &counts) { - for (const auto &entry : counts) { - if (entry.second < MAX_ONETIME_KEYS) { - const int nkeys = MAX_ONETIME_KEYS - entry.second; + uint16_t count = 0; + if (auto c = counts.find(mtx::crypto::SIGNED_CURVE25519); c != counts.end()) + count = c->second; - nhlog::crypto()->info("uploading {} {} keys", nkeys, entry.first); - olm::client()->generate_one_time_keys(nkeys); + if (count < MAX_ONETIME_KEYS) { + const int nkeys = MAX_ONETIME_KEYS - count; - http::client()->upload_keys( - olm::client()->create_upload_keys_request(), - [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { - if (err) { - nhlog::crypto()->warn( - "failed to update one-time keys: {} {} {}", - err->matrix_error.error, - static_cast(err->status_code), - static_cast(err->error_code)); + nhlog::crypto()->info( + "uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519); + olm::client()->generate_one_time_keys(nkeys); - if (err->status_code < 400 || err->status_code >= 500) - return; - } + http::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { + if (err) { + nhlog::crypto()->warn("failed to update one-time keys: {} {} {}", + err->matrix_error.error, + static_cast(err->status_code), + static_cast(err->error_code)); - // mark as published anyway, otherwise we may end up in a loop. - olm::mark_keys_as_published(); - }); - } + if (err->status_code < 400 || err->status_code >= 500) + return; + } + + // mark as published anyway, otherwise we may end up in a loop. + olm::mark_keys_as_published(); + }); } } -- cgit 1.5.1 From f4e670d8d522eafa96bddff39baa1636fa83dc57 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 25 Jul 2021 12:13:24 +0200 Subject: Fix SSSS unlock without a password set fixes #657 --- src/ChatPage.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'src/ChatPage.cpp') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 615a15b3..a76756ae 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -1028,8 +1028,15 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc); - if (!decryptionKey) - decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc); + if (!decryptionKey && keyDesc.passphrase) { + try { + decryptionKey = + mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc); + } catch (std::exception &e) { + nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", + e.what()); + } + } if (!decryptionKey) { QMessageBox::information( -- cgit 1.5.1 From 4dd994ae009b622cd35e292d1170a3f60a26c4d6 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 23 Jul 2021 18:11:33 -0400 Subject: QML the read receipts list There are probably a few things wrong with this, but I'm going to call it good enough for an initial commit --- CMakeLists.txt | 4 +- resources/qml/ReadReceipts.qml | 118 +++++++++++++++++++++++ resources/qml/Root.qml | 19 ++++ resources/qml/StatusIndicator.qml | 2 +- resources/res.qrc | 2 +- src/ChatPage.cpp | 1 - src/MainWindow.cpp | 22 ----- src/MainWindow.h | 1 - src/ReadReceiptsModel.cpp | 120 +++++++++++++++++++++++ src/ReadReceiptsModel.h | 86 +++++++++++++++++ src/dialogs/ReadReceipts.cpp | 179 ----------------------------------- src/dialogs/ReadReceipts.h | 61 ------------ src/timeline/TimelineModel.cpp | 5 +- src/timeline/TimelineModel.h | 4 +- src/timeline/TimelineViewManager.cpp | 7 ++ 15 files changed, 360 insertions(+), 271 deletions(-) create mode 100644 resources/qml/ReadReceipts.qml create mode 100644 src/ReadReceiptsModel.cpp create mode 100644 src/ReadReceiptsModel.h delete mode 100644 src/dialogs/ReadReceipts.cpp delete mode 100644 src/dialogs/ReadReceipts.h (limited to 'src/ChatPage.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b26602c..e9371579 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -286,7 +286,6 @@ set(SRC_FILES src/dialogs/Logout.cpp src/dialogs/PreviewUploadOverlay.cpp src/dialogs/ReCaptcha.cpp - src/dialogs/ReadReceipts.cpp # Emoji src/emoji/EmojiModel.cpp @@ -352,6 +351,7 @@ set(SRC_FILES src/MemberList.cpp src/MxcImageProvider.cpp src/Olm.cpp + src/ReadReceiptsModel.cpp src/RegisterPage.cpp src/SSOHandler.cpp src/CombinedImagePackModel.cpp @@ -499,7 +499,6 @@ qt5_wrap_cpp(MOC_HEADERS src/dialogs/PreviewUploadOverlay.h src/dialogs/RawMessage.h src/dialogs/ReCaptcha.h - src/dialogs/ReadReceipts.h # Emoji src/emoji/EmojiModel.h @@ -558,6 +557,7 @@ qt5_wrap_cpp(MOC_HEADERS src/MainWindow.h src/MemberList.h src/MxcImageProvider.h + src/ReadReceiptsModel.h src/RegisterPage.h src/SSOHandler.h src/CombinedImagePackModel.h diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml new file mode 100644 index 00000000..21b9b15e --- /dev/null +++ b/resources/qml/ReadReceipts.qml @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import im.nheko 1.0 + +ApplicationWindow { + id: readReceiptsRoot + + property ReadReceiptsModel readReceipts + + x: MainWindow.x + (MainWindow.width / 2) - (width / 2) + y: MainWindow.y + (MainWindow.height / 2) - (height / 2) + height: 380 + width: 340 + minimumHeight: 380 + minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium + palette: Nheko.colors + color: Nheko.colors.window + + ColumnLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + spacing: Nheko.paddingMedium + + Label { + id: headerTitle + + Layout.alignment: Qt.AlignCenter + text: qsTr("Read receipts") + font.pointSize: fontMetrics.font.pointSize * 1.5 + } + + ScrollView { + palette: Nheko.colors + padding: Nheko.paddingMedium + ScrollBar.horizontal.visible: false + Layout.fillHeight: true + Layout.minimumHeight: 200 + Layout.fillWidth: true + + ListView { + id: readReceiptsList + + clip: true + spacing: Nheko.paddingMedium + boundsBehavior: Flickable.StopAtBounds + model: readReceipts + + delegate: RowLayout { + spacing: Nheko.paddingMedium + + Avatar { + width: Nheko.avatarSize + height: Nheko.avatarSize + userid: model.mxid + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.displayName + onClicked: Rooms.currentRoom.openUserProfile(model.mxid) + ToolTip.visible: avatarHover.hovered + ToolTip.text: model.mxid + + HoverHandler { + id: avatarHover + } + + } + + ColumnLayout { + spacing: Nheko.paddingSmall + + Label { + text: model.displayName + color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window) + font.pointSize: fontMetrics.font.pointSize + ToolTip.visible: displayNameHover.hovered + ToolTip.text: model.mxid + + TapHandler { + onSingleTapped: chat.model.openUserProfile(userId) + } + + CursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + + HoverHandler { + id: displayNameHover + } + + } + + Label { + text: model.timestamp + color: Nheko.colors.buttonText + font.pointSize: fontMetrics.font.pointSize * 0.9 + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + } + + } + + } + + } + + } + +} diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index e80ff764..a099b5e6 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -96,6 +96,14 @@ Page { } + Component { + id: readReceiptsDialog + + ReadReceipts { + } + + } + Shortcut { sequence: "Ctrl+K" onActivated: { @@ -164,6 +172,17 @@ Page { target: TimelineManager } + Connections { + function onOpenReadReceiptsDialog() { + var dialog = readReceiptsDialog.createObject(timelineRoot, { + "readReceipts": rr + }); + dialog.show(); + } + + target: Rooms.currentRoom + } + Connections { function onNewInviteState() { if (CallManager.haveCallInvite && Settings.mobileMode) { diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 7e471d69..0af02b3c 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -34,7 +34,7 @@ ImageButton { } onClicked: { if (status == MtxEvent.Read) - room.readReceiptsAction(eventId); + room.showReadReceipts(eventId); } image: { diff --git a/resources/res.qrc b/resources/res.qrc index 5d37c397..2b655b9e 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -112,7 +112,6 @@ qtquickcontrols2.conf - qml/Root.qml qml/ChatPage.qml qml/CommunitiesList.qml @@ -177,6 +176,7 @@ qml/components/FlatButton.qml qml/RoomMembers.qml qml/InviteDialog.qml + qml/ReadReceipts.qml media/ring.ogg diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index a76756ae..42e3bc7b 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -31,7 +31,6 @@ #include "notifications/Manager.h" -#include "dialogs/ReadReceipts.h" #include "timeline/TimelineViewManager.h" #include "blurhash.hpp" diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index c0486d01..8bc90f29 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -36,7 +36,6 @@ #include "dialogs/JoinRoom.h" #include "dialogs/LeaveRoom.h" #include "dialogs/Logout.h" -#include "dialogs/ReadReceipts.h" MainWindow *MainWindow::instance_ = nullptr; @@ -398,27 +397,6 @@ MainWindow::openLogoutDialog() showDialog(dialog); } -void -MainWindow::openReadReceiptsDialog(const QString &event_id) -{ - auto dialog = new dialogs::ReadReceipts(this); - - const auto room_id = chat_page_->currentRoom(); - - try { - dialog->addUsers(cache::readReceipts(event_id, room_id)); - } catch (const lmdb::error &) { - nhlog::db()->warn("failed to retrieve read receipts for {} {}", - event_id.toStdString(), - chat_page_->currentRoom().toStdString()); - dialog->deleteLater(); - - return; - } - - showDialog(dialog); -} - bool MainWindow::hasActiveDialogs() const { diff --git a/src/MainWindow.h b/src/MainWindow.h index 6d62545c..d423af9f 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -65,7 +65,6 @@ public: std::function callback); void openJoinRoomDialog(std::function callback); void openLogoutDialog(); - void openReadReceiptsDialog(const QString &event_id); void hideOverlay(); void showSolidOverlayModal(QWidget *content, diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp new file mode 100644 index 00000000..293733d3 --- /dev/null +++ b/src/ReadReceiptsModel.cpp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ReadReceiptsModel.h" + +#include + +#include "Cache.h" +#include "Logging.h" +#include "Utils.h" + +ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject *parent) + : QAbstractListModel{parent} + , event_id_{event_id} + , room_id_{room_id} +{ + try { + addUsers(cache::readReceipts(event_id, room_id)); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve read receipts for {} {}", + event_id.toStdString(), + room_id_.toStdString()); + + return; + } +} + +ReadReceiptsModel::~ReadReceiptsModel() +{ + for (const auto &item : readReceipts_) + item->deleteLater(); +} + +QHash +ReadReceiptsModel::roleNames() const +{ + return {{Mxid, "mxid"}, + {DisplayName, "displayName"}, + {AvatarUrl, "avatarUrl"}, + {Timestamp, "timestamp"}}; +} + +QVariant +ReadReceiptsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0) + return {}; + + switch (role) { + case Mxid: + return readReceipts_[index.row()]->mxid(); + case DisplayName: + return readReceipts_[index.row()]->displayName(); + case AvatarUrl: + return readReceipts_[index.row()]->avatarUrl(); + case Timestamp: + // the uint64_t to QVariant conversion was ambiguous, so... + return readReceipts_[index.row()]->timestamp(); + default: + return {}; + } +} + +void +ReadReceiptsModel::addUsers( + const std::multimap> &users) +{ + std::multimap> unshown; + for (const auto &user : users) { + if (users_.find(user.first) == users_.end()) + unshown.emplace(user); + } + + beginInsertRows( + QModelIndex{}, readReceipts_.length(), readReceipts_.length() + unshown.size() - 1); + + for (const auto &user : unshown) + readReceipts_.push_back( + new ReadReceipt{QString::fromStdString(user.second), room_id_, user.first, this}); + + users_.merge(unshown); + + endInsertRows(); +} + +ReadReceipt::ReadReceipt(QString mxid, QString room_id, uint64_t timestamp, QObject *parent) + : QObject{parent} + , mxid_{mxid} + , room_id_{room_id} + , displayName_{cache::displayName(room_id_, mxid_)} + , avatarUrl_{cache::avatarUrl(room_id_, mxid_)} + , timestamp_{timestamp} +{} + +QString +ReadReceipt::timestamp() const +{ + return dateFormat(QDateTime::fromMSecsSinceEpoch(timestamp_)); +} + +QString +ReadReceipt::dateFormat(const QDateTime &then) const +{ + auto now = QDateTime::currentDateTime(); + auto days = then.daysTo(now); + + if (days == 0) + return tr("Today %1") + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + else if (days < 2) + return tr("Yesterday %1") + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + else if (days < 7) + return QString("%1 %2") + .arg(then.toString("dddd")) + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + + return QLocale::system().toString(then.time(), QLocale::ShortFormat); +} diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h new file mode 100644 index 00000000..d90bf7c1 --- /dev/null +++ b/src/ReadReceiptsModel.h @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef READRECEIPTSMODEL_H +#define READRECEIPTSMODEL_H + +#include +#include +#include + +class ReadReceipt : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString mxid READ mxid CONSTANT) + Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY(QString timestamp READ timestamp CONSTANT) + +public: + explicit ReadReceipt(QString mxid, + QString room_id, + uint64_t timestamp, + QObject *parent = nullptr); + + QString mxid() const { return mxid_; } + QString displayName() const { return displayName_; } + QString avatarUrl() const { return avatarUrl_; } + QString timestamp() const; + +signals: + void displayNameChanged(); + void avatarUrlChanged(); + +private: + QString dateFormat(const QDateTime &then) const; + + QString mxid_; + QString room_id_; + QString displayName_; + QString avatarUrl_; + uint64_t timestamp_; +}; + +class ReadReceiptsModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString eventId READ eventId CONSTANT) + Q_PROPERTY(QString roomId READ roomId CONSTANT) + +public: + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + Timestamp, + }; + + explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr); + ~ReadReceiptsModel() override; + + QString eventId() const { return event_id_; } + QString roomId() const { return room_id_; } + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent) const override + { + Q_UNUSED(parent) + return readReceipts_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + +public slots: + void addUsers(const std::multimap> &users); + +private: + QString event_id_; + QString room_id_; + QVector readReceipts_; + std::multimap> users_; +}; + +#endif // READRECEIPTSMODEL_H diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp deleted file mode 100644 index fa7132fd..00000000 --- a/src/dialogs/ReadReceipts.cpp +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dialogs/ReadReceipts.h" - -#include "AvatarProvider.h" -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Utils.h" -#include "ui/Avatar.h" - -using namespace dialogs; - -ReceiptItem::ReceiptItem(QWidget *parent, - const QString &user_id, - uint64_t timestamp, - const QString &room_id) - : QWidget(parent) -{ - topLayout_ = new QHBoxLayout(this); - topLayout_->setMargin(0); - - textLayout_ = new QVBoxLayout; - textLayout_->setMargin(0); - textLayout_->setSpacing(conf::modals::TEXT_SPACING); - - QFont nameFont; - nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); - - auto displayName = cache::displayName(room_id, user_id); - - avatar_ = new Avatar(this, 44); - avatar_->setLetter(utils::firstChar(displayName)); - - // If it's a matrix id we use the second letter. - if (displayName.size() > 1 && displayName.at(0) == '@') - avatar_->setLetter(QChar(displayName.at(1))); - - userName_ = new QLabel(displayName, this); - userName_->setFont(nameFont); - - timestamp_ = new QLabel(dateFormat(QDateTime::fromMSecsSinceEpoch(timestamp)), this); - - textLayout_->addWidget(userName_); - textLayout_->addWidget(timestamp_); - - topLayout_->addWidget(avatar_); - topLayout_->addLayout(textLayout_, 1); - - avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); -} - -void -ReceiptItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -QString -ReceiptItem::dateFormat(const QDateTime &then) const -{ - auto now = QDateTime::currentDateTime(); - auto days = then.daysTo(now); - - if (days == 0) - return tr("Today %1") - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - else if (days < 2) - return tr("Yesterday %1") - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - else if (days < 7) - return QString("%1 %2") - .arg(then.toString("dddd")) - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - - return QLocale::system().toString(then.time(), QLocale::ShortFormat); -} - -ReadReceipts::ReadReceipts(QWidget *parent) - : QFrame(parent) -{ - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); - - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); - - userList_ = new QListWidget; - userList_->setFrameStyle(QFrame::NoFrame); - userList_->setSelectionMode(QAbstractItemView::NoSelection); - userList_->setSpacing(conf::modals::TEXT_SPACING); - - QFont largeFont; - largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5); - - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - setMinimumHeight(userList_->sizeHint().height() * 2); - setMinimumWidth(std::max(userList_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN, - QFontMetrics(largeFont).averageCharWidth() * 30 - - 2 * conf::modals::WIDGET_MARGIN)); - - QFont font; - font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); - - topLabel_ = new QLabel(tr("Read receipts"), this); - topLabel_->setAlignment(Qt::AlignCenter); - topLabel_->setFont(font); - - auto okBtn = new QPushButton(tr("Close"), this); - - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(15); - buttonLayout->addStretch(1); - buttonLayout->addWidget(okBtn); - - layout->addWidget(topLabel_); - layout->addWidget(userList_); - layout->addLayout(buttonLayout); - - auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); - connect(closeShortcut, &QShortcut::activated, this, &ReadReceipts::close); - connect(okBtn, &QPushButton::clicked, this, &ReadReceipts::close); -} - -void -ReadReceipts::addUsers(const std::multimap> &receipts) -{ - // We want to remove any previous items that have been set. - userList_->clear(); - - for (const auto &receipt : receipts) { - auto user = new ReceiptItem(this, - QString::fromStdString(receipt.second), - receipt.first, - ChatPage::instance()->currentRoom()); - auto item = new QListWidgetItem(userList_); - - item->setSizeHint(user->minimumSizeHint()); - item->setFlags(Qt::NoItemFlags); - item->setTextAlignment(Qt::AlignCenter); - - userList_->setItemWidget(item, user); - } -} - -void -ReadReceipts::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -ReadReceipts::hideEvent(QHideEvent *event) -{ - userList_->clear(); - QFrame::hideEvent(event); -} diff --git a/src/dialogs/ReadReceipts.h b/src/dialogs/ReadReceipts.h deleted file mode 100644 index 5c6c5d2b..00000000 --- a/src/dialogs/ReadReceipts.h +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include - -class Avatar; -class QLabel; -class QListWidget; -class QHBoxLayout; -class QVBoxLayout; - -namespace dialogs { - -class ReceiptItem : public QWidget -{ - Q_OBJECT - -public: - ReceiptItem(QWidget *parent, - const QString &user_id, - uint64_t timestamp, - const QString &room_id); - -protected: - void paintEvent(QPaintEvent *) override; - -private: - QString dateFormat(const QDateTime &then) const; - - QHBoxLayout *topLayout_; - QVBoxLayout *textLayout_; - - Avatar *avatar_; - - QLabel *userName_; - QLabel *timestamp_; -}; - -class ReadReceipts : public QFrame -{ - Q_OBJECT -public: - explicit ReadReceipts(QWidget *parent = nullptr); - -public slots: - void addUsers(const std::multimap> &users); - -protected: - void paintEvent(QPaintEvent *event) override; - void hideEvent(QHideEvent *event) override; - -private: - QLabel *topLabel_; - - QListWidget *userList_; -}; -} // dialogs diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index ee5564a5..f5737063 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -28,6 +28,7 @@ #include "MemberList.h" #include "MxcImageProvider.h" #include "Olm.h" +#include "ReadReceiptsModel.h" #include "TimelineViewManager.h" #include "Utils.h" #include "dialogs/RawMessage.h" @@ -1089,9 +1090,9 @@ TimelineModel::relatedInfo(QString id) } void -TimelineModel::readReceiptsAction(QString id) const +TimelineModel::showReadReceipts(QString id) { - MainWindow::instance()->openReadReceiptsDialog(id); + emit openReadReceiptsDialog(new ReadReceiptsModel{id, roomId(), this}); } void diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 0e2ce153..82fce257 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -20,6 +20,7 @@ #include "InviteesModel.h" #include "MemberList.h" #include "Permissions.h" +#include "ReadReceiptsModel.h" #include "ui/RoomSettings.h" #include "ui/UserProfile.h" @@ -241,7 +242,7 @@ public: Q_INVOKABLE void openUserProfile(QString userid); Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); - Q_INVOKABLE void readReceiptsAction(QString id) const; + Q_INVOKABLE void showReadReceipts(QString id); Q_INVOKABLE void redactEvent(QString id); Q_INVOKABLE int idToIndex(QString id) const; Q_INVOKABLE QString indexToId(int index) const; @@ -348,6 +349,7 @@ signals: void typingUsersChanged(std::vector users); void replyChanged(QString reply); void editChanged(QString reply); + void openReadReceiptsDialog(ReadReceiptsModel *rr); void paginationInProgressChanged(const bool); void newCallEvent(const mtx::events::collections::TimelineEvents &event); void scrollToIndex(int index); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index a6922be7..58b0d5a8 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -26,6 +26,7 @@ #include "MainWindow.h" #include "MatrixClient.h" #include "MxcImageProvider.h" +#include "ReadReceiptsModel.h" #include "RoomsModel.h" #include "SingleImagePackModel.h" #include "UserSettingsPage.h" @@ -205,6 +206,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par 0, "InviteesModel", "InviteesModel needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "ReadReceiptsModel", + "ReadReceiptsModel needs to be instantiated on the C++ side"); static auto self = this; qmlRegisterSingletonType( -- cgit 1.5.1