From 4dd994ae009b622cd35e292d1170a3f60a26c4d6 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 23 Jul 2021 18:11:33 -0400 Subject: [PATCH 01/45] 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 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( From 774a9fdc3a5dd5221cd8143e2aaa03c7b93737f2 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 23 Jul 2021 21:58:57 -0400 Subject: [PATCH 02/45] Remove outdated comment --- src/ReadReceiptsModel.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index 293733d3..eadb4e74 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -55,7 +55,6 @@ ReadReceiptsModel::data(const QModelIndex &index, int role) const 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 {}; From 0d42909e406821b76c32b37af758a3721ea1238d Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 23 Jul 2021 22:19:48 -0400 Subject: [PATCH 03/45] Simplify read receipt storage --- src/ReadReceiptsModel.cpp | 52 +++++++++++---------------------------- src/ReadReceiptsModel.h | 41 +++--------------------------- 2 files changed, 19 insertions(+), 74 deletions(-) diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index eadb4e74..8ee9cf45 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -26,12 +26,6 @@ ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject } } -ReadReceiptsModel::~ReadReceiptsModel() -{ - for (const auto &item : readReceipts_) - item->deleteLater(); -} - QHash ReadReceiptsModel::roleNames() const { @@ -49,13 +43,13 @@ ReadReceiptsModel::data(const QModelIndex &index, int role) const switch (role) { case Mxid: - return readReceipts_[index.row()]->mxid(); + return readReceipts_[index.row()].first; case DisplayName: - return readReceipts_[index.row()]->displayName(); + return cache::displayName(room_id_, readReceipts_[index.row()].first); case AvatarUrl: - return readReceipts_[index.row()]->avatarUrl(); + return cache::avatarUrl(room_id_, readReceipts_[index.row()].first); case Timestamp: - return readReceipts_[index.row()]->timestamp(); + return dateFormat(readReceipts_[index.row()].second); default: return {}; } @@ -65,41 +59,25 @@ void ReadReceiptsModel::addUsers( const std::multimap> &users) { - std::multimap> unshown; + beginInsertRows(QModelIndex{}, readReceipts_.length(), users.size() - 1); + + readReceipts_.clear(); for (const auto &user : users) { - if (users_.find(user.first) == users_.end()) - unshown.emplace(user); + readReceipts_.push_back({QString::fromStdString(user.second), + QDateTime::fromMSecsSinceEpoch(user.first)}); } - 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); + std::sort(readReceipts_.begin(), + readReceipts_.end(), + [](const QPair &a, const QPair &b) { + return a.second > b.second; + }); 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 +ReadReceiptsModel::dateFormat(const QDateTime &then) const { auto now = QDateTime::currentDateTime(); auto days = then.daysTo(now); diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h index d90bf7c1..98e41f8f 100644 --- a/src/ReadReceiptsModel.h +++ b/src/ReadReceiptsModel.h @@ -8,40 +8,7 @@ #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_; -}; +#include class ReadReceiptsModel : public QAbstractListModel { @@ -60,7 +27,6 @@ public: }; 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_; } @@ -77,10 +43,11 @@ public slots: void addUsers(const std::multimap> &users); private: + QString dateFormat(const QDateTime &then) const; + QString event_id_; QString room_id_; - QVector readReceipts_; - std::multimap> users_; + QVector> readReceipts_; }; #endif // READRECEIPTSMODEL_H From 8a329d65174d091a5c4d1542d0e74c7d576ee3c6 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 24 Jul 2021 09:16:29 -0400 Subject: [PATCH 04/45] Remove Avatar class RIP --- CMakeLists.txt | 2 - src/MemberList.cpp | 1 - src/MemberList.h | 3 +- src/ui/Avatar.cpp | 168 --------------------------------------------- src/ui/Avatar.h | 48 ------------- 5 files changed, 2 insertions(+), 220 deletions(-) delete mode 100644 src/ui/Avatar.cpp delete mode 100644 src/ui/Avatar.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e9371579..8fc8e19d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -304,7 +304,6 @@ set(SRC_FILES src/timeline/RoomlistModel.cpp # UI components - src/ui/Avatar.cpp src/ui/Badge.cpp src/ui/DropShadow.cpp src/ui/FlatButton.cpp @@ -516,7 +515,6 @@ qt5_wrap_cpp(MOC_HEADERS src/timeline/RoomlistModel.h # UI components - src/ui/Avatar.h src/ui/Badge.h src/ui/FlatButton.h src/ui/FloatingButton.h diff --git a/src/MemberList.cpp b/src/MemberList.cpp index 0ef3b696..fb4f5ac2 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -20,7 +20,6 @@ #include "Logging.h" #include "Utils.h" #include "timeline/TimelineViewManager.h" -#include "ui/Avatar.h" MemberList::MemberList(const QString &room_id, QObject *parent) : QAbstractListModel{parent} diff --git a/src/MemberList.h b/src/MemberList.h index 9932f6a4..e6522694 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -4,9 +4,10 @@ #pragma once -#include "CacheStructs.h" #include +#include "CacheStructs.h" + class MemberList : public QAbstractListModel { Q_OBJECT diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp deleted file mode 100644 index 154a0e2c..00000000 --- a/src/ui/Avatar.cpp +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include - -#include "AvatarProvider.h" -#include "Utils.h" -#include "ui/Avatar.h" - -Avatar::Avatar(QWidget *parent, int size) - : QWidget(parent) - , size_(size) -{ - type_ = ui::AvatarType::Letter; - letter_ = "A"; - - QFont _font(font()); - _font.setPointSizeF(ui::FontSize); - setFont(_font); - - QSizePolicy policy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); - setSizePolicy(policy); -} - -QColor -Avatar::textColor() const -{ - if (!text_color_.isValid()) - return QColor("black"); - - return text_color_; -} - -QColor -Avatar::backgroundColor() const -{ - if (!text_color_.isValid()) - return QColor("white"); - - return background_color_; -} - -QSize -Avatar::sizeHint() const -{ - return QSize(size_ + 2, size_ + 2); -} - -void -Avatar::setTextColor(const QColor &color) -{ - text_color_ = color; -} - -void -Avatar::setBackgroundColor(const QColor &color) -{ - background_color_ = color; -} - -void -Avatar::setLetter(const QString &letter) -{ - letter_ = letter; - type_ = ui::AvatarType::Letter; - update(); -} - -void -Avatar::setImage(const QString &avatar_url) -{ - avatar_url_ = avatar_url; - AvatarProvider::resolve(avatar_url, - static_cast(size_ * pixmap_.devicePixelRatio()), - this, - [this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) { - if (pm.isNull()) - return; - type_ = ui::AvatarType::Image; - pixmap_ = pm; - pixmap_.setDevicePixelRatio(requestedRatio); - update(); - }); -} - -void -Avatar::setImage(const QString &room, const QString &user) -{ - room_ = room; - user_ = user; - AvatarProvider::resolve(room, - user, - static_cast(size_ * pixmap_.devicePixelRatio()), - this, - [this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) { - if (pm.isNull()) - return; - type_ = ui::AvatarType::Image; - pixmap_ = pm; - pixmap_.setDevicePixelRatio(requestedRatio); - update(); - }); -} - -void -Avatar::setDevicePixelRatio(double ratio) -{ - if (type_ == ui::AvatarType::Image && abs(pixmap_.devicePixelRatio() - ratio) > 0.01) { - pixmap_ = pixmap_.scaled(QSize(size_, size_) * ratio); - pixmap_.setDevicePixelRatio(ratio); - - if (!avatar_url_.isEmpty()) - setImage(avatar_url_); - else - setImage(room_, user_); - } -} - -void -Avatar::paintEvent(QPaintEvent *) -{ - bool rounded = QSettings().value(QStringLiteral("user/avatar_circles"), true).toBool(); - - QPainter painter(this); - - painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | - QPainter::TextAntialiasing); - - QRectF r = rect(); - const int hs = size_ / 2; - - if (type_ != ui::AvatarType::Image) { - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(backgroundColor()); - - painter.setPen(Qt::NoPen); - painter.setBrush(brush); - rounded ? painter.drawEllipse(r) : painter.drawRoundedRect(r, 3, 3); - } else if (painter.isActive()) { - setDevicePixelRatio(painter.device()->devicePixelRatioF()); - } - - switch (type_) { - case ui::AvatarType::Image: { - QPainterPath ppath; - - rounded ? ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_) - : ppath.addRoundedRect(r, 3, 3); - - painter.setClipPath(ppath); - painter.drawPixmap(QRect(width() / 2 - hs, height() / 2 - hs, size_, size_), - pixmap_); - break; - } - case ui::AvatarType::Letter: { - painter.setPen(textColor()); - painter.setBrush(Qt::NoBrush); - painter.drawText(r.translated(0, -1), Qt::AlignCenter, letter_); - break; - } - default: - break; - } -} diff --git a/src/ui/Avatar.h b/src/ui/Avatar.h deleted file mode 100644 index bbf05be3..00000000 --- a/src/ui/Avatar.h +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include - -#include "Theme.h" - -class Avatar : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - explicit Avatar(QWidget *parent = nullptr, int size = ui::AvatarSize); - - void setBackgroundColor(const QColor &color); - void setImage(const QString &avatar_url); - void setImage(const QString &room, const QString &user); - void setLetter(const QString &letter); - void setTextColor(const QColor &color); - void setDevicePixelRatio(double ratio); - - QColor backgroundColor() const; - QColor textColor() const; - - QSize sizeHint() const override; - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void init(); - - ui::AvatarType type_; - QString letter_; - QString avatar_url_, room_, user_; - QColor background_color_; - QColor text_color_; - QPixmap pixmap_; - int size_; -}; From 9c7bde22d10eef049c4fc267b3149db1b2f22343 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 24 Jul 2021 09:16:58 -0400 Subject: [PATCH 05/45] Remove unused headers Why didn't I see these earlier? --- src/MemberList.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/MemberList.cpp b/src/MemberList.cpp index fb4f5ac2..196647fe 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -2,16 +2,6 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include "MemberList.h" #include "Cache.h" From 2be91b591dde67377cdd0c6125b8faf5a6b79cdd Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 24 Jul 2021 09:17:06 -0400 Subject: [PATCH 06/45] make lint --- src/ReadReceiptsModel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h index 98e41f8f..d7ff5fb8 100644 --- a/src/ReadReceiptsModel.h +++ b/src/ReadReceiptsModel.h @@ -6,9 +6,9 @@ #define READRECEIPTSMODEL_H #include +#include #include #include -#include class ReadReceiptsModel : public QAbstractListModel { From b03a1df19da3e7e6732cff7b21743d214336d00d Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 24 Jul 2021 12:51:45 -0400 Subject: [PATCH 07/45] Add close button at footer --- resources/qml/ReadReceipts.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml index 21b9b15e..b3bca9db 100644 --- a/resources/qml/ReadReceipts.qml +++ b/resources/qml/ReadReceipts.qml @@ -115,4 +115,10 @@ ApplicationWindow { } + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok + onAccepted: readReceiptsRoot.close() + + } + } From 3ce7fdd63fda4fa31ee444448c829debe5e408f2 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 24 Jul 2021 14:42:40 -0400 Subject: [PATCH 08/45] Fix incorrect function name --- resources/qml/MessageView.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 07feec8c..b6f2b909 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -580,7 +580,7 @@ ScrollView { Platform.MenuItem { text: qsTr("Read receip&ts") - onTriggered: room.readReceiptsAction(messageContextMenu.eventId) + onTriggered: room.showReadReceipts(messageContextMenu.eventId) } Platform.MenuItem { From 2fe010c04a90bb232f077a513a7ef6e31a97621a Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 24 Jul 2021 15:35:28 -0400 Subject: [PATCH 09/45] Dynamically update read receipts --- resources/qml/ReadReceipts.qml | 1 - src/ReadReceiptsModel.cpp | 27 ++++++++++++++++++++++++--- src/ReadReceiptsModel.h | 1 + 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml index b3bca9db..0756a2e7 100644 --- a/resources/qml/ReadReceipts.qml +++ b/resources/qml/ReadReceipts.qml @@ -118,7 +118,6 @@ ApplicationWindow { footer: DialogButtonBox { standardButtons: DialogButtonBox.Ok onAccepted: readReceiptsRoot.close() - } } diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index 8ee9cf45..8a371922 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -7,6 +7,7 @@ #include #include "Cache.h" +#include "Cache_p.h" #include "Logging.h" #include "Utils.h" @@ -16,10 +17,26 @@ ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject , room_id_{room_id} { try { - addUsers(cache::readReceipts(event_id, room_id)); + addUsers(cache::readReceipts(event_id_, room_id_)); } catch (const lmdb::error &) { nhlog::db()->warn("failed to retrieve read receipts for {} {}", - event_id.toStdString(), + event_id_.toStdString(), + room_id_.toStdString()); + + return; + } + + connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update); +} + +void +ReadReceiptsModel::update() +{ + 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; @@ -59,7 +76,9 @@ void ReadReceiptsModel::addUsers( const std::multimap> &users) { - beginInsertRows(QModelIndex{}, readReceipts_.length(), users.size() - 1); + auto oldLen = readReceipts_.length(); + + beginInsertRows(QModelIndex{}, oldLen, users.size() - 1); readReceipts_.clear(); for (const auto &user : users) { @@ -74,6 +93,8 @@ ReadReceiptsModel::addUsers( }); endInsertRows(); + + emit dataChanged(index(0), index(oldLen - 1)); } QString diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h index d7ff5fb8..f2e39f88 100644 --- a/src/ReadReceiptsModel.h +++ b/src/ReadReceiptsModel.h @@ -41,6 +41,7 @@ public: public slots: void addUsers(const std::multimap> &users); + void update(); private: QString dateFormat(const QDateTime &then) const; From 9dc9152e075804200158a1f6c6fb8f6e10961221 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 24 Jul 2021 18:38:22 -0400 Subject: [PATCH 10/45] Close dialog on escape --- resources/qml/ReadReceipts.qml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml index 0756a2e7..da2a5f66 100644 --- a/resources/qml/ReadReceipts.qml +++ b/resources/qml/ReadReceipts.qml @@ -21,6 +21,11 @@ ApplicationWindow { palette: Nheko.colors color: Nheko.colors.window + Shortcut { + sequence: StandardKey.Cancel + onActivated: readReceiptsRoot.close() + } + ColumnLayout { anchors.fill: parent anchors.margins: Nheko.paddingMedium From 5d38b96bbba01369e3b0238579bab64f74079fc5 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 28 Jul 2021 17:50:49 -0400 Subject: [PATCH 11/45] Use Dialog flag to make tiling WMs happy --- resources/qml/ReadReceipts.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml index da2a5f66..84dc5666 100644 --- a/resources/qml/ReadReceipts.qml +++ b/resources/qml/ReadReceipts.qml @@ -20,6 +20,7 @@ ApplicationWindow { minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium palette: Nheko.colors color: Nheko.colors.window + flags: Qt.Dialog Shortcut { sequence: StandardKey.Cancel From 1777a1b52ffcb4e2d3fa0c394b14b3282ef6f3d5 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 28 Jul 2021 18:20:23 -0400 Subject: [PATCH 12/45] Reset model instead of doing weird convoluted updates --- src/ReadReceiptsModel.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index 8a371922..936c6d61 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -76,9 +76,7 @@ void ReadReceiptsModel::addUsers( const std::multimap> &users) { - auto oldLen = readReceipts_.length(); - - beginInsertRows(QModelIndex{}, oldLen, users.size() - 1); + beginResetModel(); readReceipts_.clear(); for (const auto &user : users) { @@ -92,9 +90,7 @@ ReadReceiptsModel::addUsers( return a.second > b.second; }); - endInsertRows(); - - emit dataChanged(index(0), index(oldLen - 1)); + endResetModel(); } QString From 7e538851d6e3779434722e56a968e9f8b8a9da0d Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 28 Jul 2021 21:31:37 -0400 Subject: [PATCH 13/45] Use a QSortFilterProxyModel instead of resetting the model --- CMakeLists.txt | 4 +- resources/qml/ReadReceipts.qml | 4 +- src/ReadReceiptsModel.cpp | 55 +++++++++++++++++++--------- src/ReadReceiptsModel.h | 27 ++++++++++++-- src/timeline/TimelineModel.cpp | 2 +- src/timeline/TimelineModel.h | 2 +- src/timeline/TimelineViewManager.cpp | 6 +-- 7 files changed, 71 insertions(+), 29 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8fc8e19d..80ea628f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -350,7 +350,7 @@ set(SRC_FILES src/MemberList.cpp src/MxcImageProvider.cpp src/Olm.cpp - src/ReadReceiptsModel.cpp + src/ReadReceiptsModel.cpp src/RegisterPage.cpp src/SSOHandler.cpp src/CombinedImagePackModel.cpp @@ -555,7 +555,7 @@ qt5_wrap_cpp(MOC_HEADERS src/MainWindow.h src/MemberList.h src/MxcImageProvider.h - src/ReadReceiptsModel.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 index 84dc5666..5f213328 100644 --- a/resources/qml/ReadReceipts.qml +++ b/resources/qml/ReadReceipts.qml @@ -10,7 +10,7 @@ import im.nheko 1.0 ApplicationWindow { id: readReceiptsRoot - property ReadReceiptsModel readReceipts + property ReadReceiptsProxy readReceipts x: MainWindow.x + (MainWindow.width / 2) - (width / 2) y: MainWindow.y + (MainWindow.height / 2) - (height / 2) @@ -86,7 +86,7 @@ ApplicationWindow { ToolTip.text: model.mxid TapHandler { - onSingleTapped: chat.model.openUserProfile(userId) + onSingleTapped: Rooms.currentRoom.openUserProfile(userId) } CursorShape { diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index 936c6d61..0be22be2 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -46,10 +46,13 @@ ReadReceiptsModel::update() QHash ReadReceiptsModel::roleNames() const { - return {{Mxid, "mxid"}, - {DisplayName, "displayName"}, - {AvatarUrl, "avatarUrl"}, - {Timestamp, "timestamp"}}; + // Note: RawTimestamp is purposely not included here + return { + {Mxid, "mxid"}, + {DisplayName, "displayName"}, + {AvatarUrl, "avatarUrl"}, + {Timestamp, "timestamp"}, + }; } QVariant @@ -67,6 +70,8 @@ ReadReceiptsModel::data(const QModelIndex &index, int role) const return cache::avatarUrl(room_id_, readReceipts_[index.row()].first); case Timestamp: return dateFormat(readReceipts_[index.row()].second); + case RawTimestamp: + return readReceipts_[index.row()].second; default: return {}; } @@ -76,21 +81,22 @@ void ReadReceiptsModel::addUsers( const std::multimap> &users) { - beginResetModel(); + auto newReceipts = users.size() - readReceipts_.size(); - readReceipts_.clear(); - for (const auto &user : users) { - readReceipts_.push_back({QString::fromStdString(user.second), - QDateTime::fromMSecsSinceEpoch(user.first)}); + if (newReceipts > 0) { + beginInsertRows( + QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1); + + for (const auto &user : users) { + QPair item = { + QString::fromStdString(user.second), + QDateTime::fromMSecsSinceEpoch(user.first)}; + if (!readReceipts_.contains(item)) + readReceipts_.push_back(item); + } + + endInsertRows(); } - - std::sort(readReceipts_.begin(), - readReceipts_.end(), - [](const QPair &a, const QPair &b) { - return a.second > b.second; - }); - - endResetModel(); } QString @@ -112,3 +118,18 @@ ReadReceiptsModel::dateFormat(const QDateTime &then) const return QLocale::system().toString(then.time(), QLocale::ShortFormat); } + +ReadReceiptsProxy::ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent) + : QSortFilterProxyModel{parent} + , model_{event_id, room_id, this} +{ + setSourceModel(&model_); + setSortRole(ReadReceiptsModel::RawTimestamp); +} + +bool +ReadReceiptsProxy::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const +{ + // since we are sorting from greatest to least timestamp, return something that looks totally backwards! + return source_left.data().toULongLong() > source_right.data().toULongLong(); +} diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h index f2e39f88..9e26bcd5 100644 --- a/src/ReadReceiptsModel.h +++ b/src/ReadReceiptsModel.h @@ -8,15 +8,13 @@ #include #include #include +#include #include class ReadReceiptsModel : public QAbstractListModel { Q_OBJECT - Q_PROPERTY(QString eventId READ eventId CONSTANT) - Q_PROPERTY(QString roomId READ roomId CONSTANT) - public: enum Roles { @@ -24,6 +22,7 @@ public: DisplayName, AvatarUrl, Timestamp, + RawTimestamp, }; explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr); @@ -51,4 +50,26 @@ private: QVector> readReceipts_; }; +class ReadReceiptsProxy : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QString eventId READ eventId CONSTANT) + Q_PROPERTY(QString roomId READ roomId CONSTANT) + +public: + explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr); + + QString eventId() const { return event_id_; } + QString roomId() const { return room_id_; } + + bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const; + +private: + QString event_id_; + QString room_id_; + + ReadReceiptsModel model_; +}; + #endif // READRECEIPTSMODEL_H diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index f5737063..6ae0c4d1 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1092,7 +1092,7 @@ TimelineModel::relatedInfo(QString id) void TimelineModel::showReadReceipts(QString id) { - emit openReadReceiptsDialog(new ReadReceiptsModel{id, roomId(), this}); + emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this}); } void diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 82fce257..0d5f7109 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -349,7 +349,7 @@ signals: void typingUsersChanged(std::vector users); void replyChanged(QString reply); void editChanged(QString reply); - void openReadReceiptsDialog(ReadReceiptsModel *rr); + void openReadReceiptsDialog(ReadReceiptsProxy *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 58b0d5a8..76bc127e 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -206,12 +206,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par 0, "InviteesModel", "InviteesModel needs to be instantiated on the C++ side"); - qmlRegisterUncreatableType( + qmlRegisterUncreatableType( "im.nheko", 1, 0, - "ReadReceiptsModel", - "ReadReceiptsModel needs to be instantiated on the C++ side"); + "ReadReceiptsProxy", + "ReadReceiptsProxy needs to be instantiated on the C++ side"); static auto self = this; qmlRegisterSingletonType( From 368e13fac38e22b5c0ce4669de63dcdadbb31116 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 29 Jul 2021 20:49:37 -0400 Subject: [PATCH 14/45] Use built-in sorting so that dynamic updates work --- src/ReadReceiptsModel.cpp | 9 ++------- src/ReadReceiptsModel.h | 2 -- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index 0be22be2..d8b7141f 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -125,11 +125,6 @@ ReadReceiptsProxy::ReadReceiptsProxy(QString event_id, QString room_id, QObject { setSourceModel(&model_); setSortRole(ReadReceiptsModel::RawTimestamp); -} - -bool -ReadReceiptsProxy::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const -{ - // since we are sorting from greatest to least timestamp, return something that looks totally backwards! - return source_left.data().toULongLong() > source_right.data().toULongLong(); + sort(0, Qt::DescendingOrder); + setDynamicSortFilter(true); } diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h index 9e26bcd5..3b45716c 100644 --- a/src/ReadReceiptsModel.h +++ b/src/ReadReceiptsModel.h @@ -63,8 +63,6 @@ public: QString eventId() const { return event_id_; } QString roomId() const { return room_id_; } - bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const; - private: QString event_id_; QString room_id_; From 135622e14e8ff3bba32becce722d986e0abf11f5 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 29 Jul 2021 21:29:09 -0400 Subject: [PATCH 15/45] Don't switch room that read receipt-related stuff is opened in --- resources/qml/ReadReceipts.qml | 5 +++-- resources/qml/Root.qml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml index 5f213328..db5d2e36 100644 --- a/resources/qml/ReadReceipts.qml +++ b/resources/qml/ReadReceipts.qml @@ -11,6 +11,7 @@ ApplicationWindow { id: readReceiptsRoot property ReadReceiptsProxy readReceipts + property Room room x: MainWindow.x + (MainWindow.width / 2) - (width / 2) y: MainWindow.y + (MainWindow.height / 2) - (height / 2) @@ -65,7 +66,7 @@ ApplicationWindow { userid: model.mxid url: model.avatarUrl.replace("mxc://", "image://MxcImage/") displayName: model.displayName - onClicked: Rooms.currentRoom.openUserProfile(model.mxid) + onClicked: room.openUserProfile(model.mxid) ToolTip.visible: avatarHover.hovered ToolTip.text: model.mxid @@ -86,7 +87,7 @@ ApplicationWindow { ToolTip.text: model.mxid TapHandler { - onSingleTapped: Rooms.currentRoom.openUserProfile(userId) + onSingleTapped: room.openUserProfile(userId) } CursorShape { diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index a099b5e6..a7684af5 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -173,9 +173,10 @@ Page { } Connections { - function onOpenReadReceiptsDialog() { + function onOpenReadReceiptsDialog(rr) { var dialog = readReceiptsDialog.createObject(timelineRoot, { - "readReceipts": rr + "readReceipts": rr, + "room": Rooms.currentRoom }); dialog.show(); } From 6409462a9643531218b4085385806779f7a22fd8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 30 Jul 2021 03:31:29 +0200 Subject: [PATCH 16/45] Rate limit olm session creation --- src/Olm.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Olm.cpp b/src/Olm.cpp index d20bf9a4..d421e336 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -1138,9 +1138,23 @@ send_encrypted_to_device_messages(const std::map, qint64> rateLimit; + auto currentTime = QDateTime::currentSecsSinceEpoch(); + if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 < + currentTime) { + claims.one_time_keys[user][device] = + mtx::crypto::SIGNED_CURVE25519; + pks[user][device].ed25519 = d.keys.at("ed25519:" + device); + pks[user][device].curve25519 = + d.keys.at("curve25519:" + device); + + rateLimit.insert(QPair(user, device), currentTime); + } else { + nhlog::crypto()->warn("Not creating new session with {}:{} " + "because of rate limit", + user, + device); + } continue; } From e4cd8b1c11864ab4d23c241ed3c519e2b1f067e5 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 30 Jul 2021 03:31:49 +0200 Subject: [PATCH 17/45] Log how many rooms we loaded --- src/timeline/RoomlistModel.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index f7f377fb..f4c927ac 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -533,6 +533,8 @@ RoomlistModel::initializeRooms() for (const auto &id : cache::client()->roomIds()) addRoom(id, true); + nhlog::db()->info("Restored {} rooms from cache", rowCount()); + endResetModel(); } From e7877ae5af533f75ee8fade52f968c453df7c201 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 30 Jul 2021 12:44:08 +0200 Subject: [PATCH 18/45] Fix crash when we don't have keys for other device when receiving an olm message from it --- src/Olm.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Olm.cpp b/src/Olm.cpp index d421e336..e3ca1c34 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -286,11 +286,17 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey bool from_their_device = false; for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { - if (key.keys.at("curve25519:" + device_id) == msg.sender_key) { - if (key.keys.at("ed25519:" + device_id) == sender_ed25519) { - from_their_device = true; - break; - } + auto c_key = key.keys.find("curve25519:" + device_id); + auto e_key = key.keys.find("ed25519:" + device_id); + + if (c_key == key.keys.end() || e_key == key.keys.end()) { + nhlog::crypto()->warn( + "Skipping device {} as we have no keys for it.", + device_id); + } else if (c_key->second == msg.sender_key && + e_key->second == sender_ed25519) { + from_their_device = true; + break; } } if (!from_their_device) { From 330b9d62a580fbd4c79925f511ab4c2e2200ad60 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 30 Jul 2021 07:24:48 -0400 Subject: [PATCH 19/45] Move read receipts connection to allow for future pop-out room views --- resources/qml/Root.qml | 12 ------------ resources/qml/TimelineView.qml | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index a7684af5..7d91beae 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -172,18 +172,6 @@ Page { target: TimelineManager } - Connections { - function onOpenReadReceiptsDialog(rr) { - var dialog = readReceiptsDialog.createObject(timelineRoot, { - "readReceipts": rr, - "room": Rooms.currentRoom - }); - dialog.show(); - } - - target: Rooms.currentRoom - } - Connections { function onNewInviteState() { if (CallManager.haveCallInvite && Settings.mobileMode) { diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index c5cc69a6..d19f2cc9 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -249,4 +249,16 @@ Item { roomid: room ? room.roomId : "" } + Connections { + function onOpenReadReceiptsDialog(rr) { + var dialog = readReceiptsDialog.createObject(timelineRoot, { + "readReceipts": rr, + "room": room + }); + dialog.show(); + } + + target: room + } + } From 3cb4209d7b3c6a0b8455f49b991b897adf302572 Mon Sep 17 00:00:00 2001 From: Loren Burkholder <55629213+LorenDB@users.noreply.github.com> Date: Fri, 30 Jul 2021 07:56:25 -0400 Subject: [PATCH 20/45] Reformat dates Co-authored-by: DeepBlueV7.X --- src/ReadReceiptsModel.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index d8b7141f..562353a7 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -106,13 +106,14 @@ ReadReceiptsModel::dateFormat(const QDateTime &then) const auto days = then.daysTo(now); if (days == 0) - return tr("Today %1") + return tr("Today, %1") .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); else if (days < 2) - return tr("Yesterday %1") + return tr("Yesterday, %1") .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); else if (days < 7) - return QString("%1 %2") + //: %1 is the name of the current day, %2 is the time the read receipt was read. The result may look like this: Monday, 7:15 + return QString("%1, %2") .arg(then.toString("dddd")) .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); From b398454409e3197ebf1b7d501e106bbb46523073 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 30 Jul 2021 08:14:44 -0400 Subject: [PATCH 21/45] Use an explicit color for the label --- resources/qml/ReadReceipts.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml index db5d2e36..8869d813 100644 --- a/resources/qml/ReadReceipts.qml +++ b/resources/qml/ReadReceipts.qml @@ -36,6 +36,7 @@ ApplicationWindow { Label { id: headerTitle + color: Nheko.colors.text Layout.alignment: Qt.AlignCenter text: qsTr("Read receipts") font.pointSize: fontMetrics.font.pointSize * 1.5 From 7dcdd51a8b329178152526dee875ba4980e4993d Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 30 Jul 2021 08:19:05 -0400 Subject: [PATCH 22/45] make lint --- src/ReadReceiptsModel.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index 562353a7..059f5d53 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -112,7 +112,8 @@ ReadReceiptsModel::dateFormat(const QDateTime &then) const return tr("Yesterday, %1") .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); else if (days < 7) - //: %1 is the name of the current day, %2 is the time the read receipt was read. The result may look like this: Monday, 7:15 + //: %1 is the name of the current day, %2 is the time the read receipt was read. The + //: result may look like this: Monday, 7:15 return QString("%1, %2") .arg(then.toString("dddd")) .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); From f48f244dcbfa319c5b8092791231fe56ac70bb8d Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 30 Jul 2021 08:44:07 -0400 Subject: [PATCH 23/45] Use correct date format --- src/ReadReceiptsModel.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index 059f5d53..25262c59 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -106,8 +106,7 @@ ReadReceiptsModel::dateFormat(const QDateTime &then) const auto days = then.daysTo(now); if (days == 0) - return tr("Today, %1") - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + return QLocale::system().toString(then.time(), QLocale::ShortFormat); else if (days < 2) return tr("Yesterday, %1") .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); From 5b0bd26795abdf222d0cfd3e5ee3cf8e8b41a9c9 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 31 Jul 2021 11:04:47 +0200 Subject: [PATCH 24/45] Fix annoying touch overlap in room list --- resources/qml/MessageView.qml | 4 ++-- resources/qml/RoomList.qml | 43 ++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index b6f2b909..9ba5e2d0 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -212,9 +212,9 @@ ScrollView { // force current read index to update onTriggered: { - if (chat.model) { + if (chat.model) chat.model.setCurrentIndex(chat.model.currentIndex); - } + } interval: 1000 } diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index a2e50fab..695b08f3 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -172,31 +172,38 @@ Page { } ] - TapHandler { - margin: -Nheko.paddingSmall - acceptedButtons: Qt.RightButton - onSingleTapped: { - if (!TimelineManager.isInvite) - roomContextMenu.show(roomId, tags); + // NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that... + Item { + anchors.fill: parent + anchors.margins: 1 + TapHandler { + acceptedButtons: Qt.RightButton + onSingleTapped: { + if (!TimelineManager.isInvite) + roomContextMenu.show(roomId, tags); + + } + gesturePolicy: TapHandler.ReleaseWithinBounds + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | DeviceType.TouchPad } - gesturePolicy: TapHandler.ReleaseWithinBounds - } - TapHandler { - margin: -Nheko.paddingSmall - onSingleTapped: Rooms.setCurrentRoom(roomId) - onLongPressed: { - if (!isInvite) - roomContextMenu.show(roomId, tags); + TapHandler { + margin: -Nheko.paddingSmall + onSingleTapped: Rooms.setCurrentRoom(roomId) + onLongPressed: { + if (!isInvite) + roomContextMenu.show(roomId, tags); + } } - } - HoverHandler { - id: hovered + HoverHandler { + id: hovered + + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | DeviceType.TouchPad + } - margin: -Nheko.paddingSmall } RowLayout { From 4c151cc3c7a6722930ea2b957d63204dd62b15ed Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 31 Jul 2021 15:59:19 +0200 Subject: [PATCH 25/45] Fix C&P error for DeviceType --- resources/qml/RoomList.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 695b08f3..cbc65fc0 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -185,7 +185,7 @@ Page { } gesturePolicy: TapHandler.ReleaseWithinBounds - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | DeviceType.TouchPad + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad } TapHandler { @@ -201,7 +201,7 @@ Page { HoverHandler { id: hovered - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | DeviceType.TouchPad + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad } } From 760f6757923c9aba2ecd7d89fc0ddfd43a741c6d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 31 Jul 2021 17:59:03 +0200 Subject: [PATCH 26/45] Ensure the encrypted rooms db is always created --- src/Cache.cpp | 9 +++++---- src/Cache_p.h | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Cache.cpp b/src/Cache.cpp index 4c24a712..7d0b1a89 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -288,6 +288,9 @@ Cache::setup() outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE); + // What rooms are encrypted + encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); + txn.commit(); databaseReady_ = true; @@ -298,8 +301,7 @@ Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) { nhlog::db()->info("mark room {} as encrypted", room_id); - auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); - db.put(txn, room_id, "0"); + encryptedRooms_.put(txn, room_id, "0"); } bool @@ -308,8 +310,7 @@ Cache::isRoomEncrypted(const std::string &room_id) std::string_view unused; auto txn = ro_txn(env_); - auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); - auto res = db.get(txn, room_id, unused); + auto res = encryptedRooms_.get(txn, room_id, unused); return res; } diff --git a/src/Cache_p.h b/src/Cache_p.h index 89c88925..18b9601f 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -689,6 +689,8 @@ private: lmdb::dbi outboundMegolmSessionDb_; lmdb::dbi megolmSessionDataDb_; + lmdb::dbi encryptedRooms_; + QString localUserId_; QString cacheDirectory_; From dab1c9068ac6d48a1faba54d7510deb360ae74e3 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 30 Jul 2021 22:13:58 -0400 Subject: [PATCH 27/45] QML the raw message dialog --- CMakeLists.txt | 1 - resources/qml/RawMessageDialog.qml | 46 +++++++++++++++++++++++ resources/qml/Root.qml | 8 ++++ resources/qml/TimelineView.qml | 7 ++++ resources/res.qrc | 1 + src/dialogs/RawMessage.h | 60 ------------------------------ src/timeline/TimelineModel.cpp | 11 ++---- src/timeline/TimelineModel.h | 5 ++- src/ui/NhekoGlobalObject.h | 5 +++ 9 files changed, 74 insertions(+), 70 deletions(-) create mode 100644 resources/qml/RawMessageDialog.qml delete mode 100644 src/dialogs/RawMessage.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 80ea628f..9f824048 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -496,7 +496,6 @@ qt5_wrap_cpp(MOC_HEADERS src/dialogs/LeaveRoom.h src/dialogs/Logout.h src/dialogs/PreviewUploadOverlay.h - src/dialogs/RawMessage.h src/dialogs/ReCaptcha.h # Emoji diff --git a/resources/qml/RawMessageDialog.qml b/resources/qml/RawMessageDialog.qml new file mode 100644 index 00000000..62a5770f --- /dev/null +++ b/resources/qml/RawMessageDialog.qml @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import im.nheko 1.0 + +ApplicationWindow { + id: rawMessageRoot + + property alias rawMessage: rawMessageView.text + + x: MainWindow.x + (MainWindow.width / 2) - (width / 2) + y: MainWindow.y + (MainWindow.height / 2) - (height / 2) + height: 420 + width: 420 + palette: Nheko.colors + color: Nheko.colors.window + flags: Qt.Tool | Qt.WindowStaysOnTopHint + + Shortcut { + sequence: StandardKey.Cancel + onActivated: rawMessageRoot.close() + } + + ScrollView { + anchors.fill: parent + palette: Nheko.colors + padding: Nheko.paddingMedium + + TextArea { + id: rawMessageView + + font: Nheko.monospaceFont() + palette: Nheko.colors + readOnly: true + } + + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok + onAccepted: rawMessageRoot.close() + } +} diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 7d91beae..70cfbda5 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -104,6 +104,14 @@ Page { } + Component { + id: rawMessageDialog + + RawMessageDialog { + } + + } + Shortcut { sequence: "Ctrl+K" onActivated: { diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index d19f2cc9..e4036eb7 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -258,6 +258,13 @@ Item { dialog.show(); } + function onShowRawMessageDialog(rawMessage) { + var dialog = rawMessageDialog.createObject(timelineRoot, { + "rawMessage": rawMessage + }); + dialog.show(); + } + target: room } diff --git a/resources/res.qrc b/resources/res.qrc index 2b655b9e..c911653c 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -177,6 +177,7 @@ qml/RoomMembers.qml qml/InviteDialog.qml qml/ReadReceipts.qml + qml/RawMessageDialog.qml media/ring.ogg diff --git a/src/dialogs/RawMessage.h b/src/dialogs/RawMessage.h deleted file mode 100644 index e95f675c..00000000 --- a/src/dialogs/RawMessage.h +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include -#include -#include - -#include "nlohmann/json.hpp" - -#include "Logging.h" -#include "MainWindow.h" -#include "ui/FlatButton.h" - -namespace dialogs { - -class RawMessage : public QWidget -{ - Q_OBJECT -public: - RawMessage(QString msg, QWidget *parent = nullptr) - : QWidget{parent} - { - QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); - - auto layout = new QVBoxLayout{this}; - auto viewer = new QTextBrowser{this}; - viewer->setFont(monospaceFont); - viewer->setText(msg); - - layout->setSpacing(0); - layout->setMargin(0); - layout->addWidget(viewer); - - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setAttribute(Qt::WA_DeleteOnClose, true); - - QSize winsize; - QPoint center; - - auto window = MainWindow::instance(); - if (window) { - winsize = window->frameGeometry().size(); - center = window->frameGeometry().center(); - - move(center.x() - (width() * 0.5), center.y() - (height() * 0.5)); - } else { - nhlog::ui()->warn("unable to retrieve MainWindow's size"); - } - - raise(); - show(); - } -}; -} // namespace dialogs diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 6ae0c4d1..a8adf05b 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -31,7 +31,6 @@ #include "ReadReceiptsModel.h" #include "TimelineViewManager.h" #include "Utils.h" -#include "dialogs/RawMessage.h" Q_DECLARE_METATYPE(QModelIndex) @@ -1026,14 +1025,13 @@ TimelineModel::formatDateSeparator(QDate date) const } void -TimelineModel::viewRawMessage(QString id) const +TimelineModel::viewRawMessage(QString id) { auto e = events.get(id.toStdString(), "", false); if (!e) return; std::string ev = mtx::accessors::serialize_event(*e).dump(4); - auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); - Q_UNUSED(dialog); + emit showRawMessageDialog(QString::fromStdString(ev)); } void @@ -1047,15 +1045,14 @@ TimelineModel::forwardMessage(QString eventId, QString roomId) } void -TimelineModel::viewDecryptedRawMessage(QString id) const +TimelineModel::viewDecryptedRawMessage(QString id) { auto e = events.get(id.toStdString(), ""); if (!e) return; std::string ev = mtx::accessors::serialize_event(*e).dump(4); - auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); - Q_UNUSED(dialog); + emit showRawMessageDialog(QString::fromStdString(ev)); } void diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 0d5f7109..f62c5360 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -236,9 +236,9 @@ public: Q_INVOKABLE QString formatGuestAccessEvent(QString id); Q_INVOKABLE QString formatPowerLevelEvent(QString id); - Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void viewRawMessage(QString id); Q_INVOKABLE void forwardMessage(QString eventId, QString roomId); - Q_INVOKABLE void viewDecryptedRawMessage(QString id) const; + Q_INVOKABLE void viewDecryptedRawMessage(QString id); Q_INVOKABLE void openUserProfile(QString userid); Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); @@ -350,6 +350,7 @@ signals: void replyChanged(QString reply); void editChanged(QString reply); void openReadReceiptsDialog(ReadReceiptsProxy *rr); + void showRawMessageDialog(QString rawMessage); void paginationInProgressChanged(const bool); void newCallEvent(const mtx::events::collections::TimelineEvents &event); void scrollToIndex(int index); diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index 14135fd1..cfe982c5 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -38,6 +39,10 @@ public: int paddingLarge() const { return 20; } UserProfile *currentUser() const; + Q_INVOKABLE QFont monospaceFont() const + { + return QFontDatabase::systemFont(QFontDatabase::FixedFont); + } Q_INVOKABLE void openLink(QString link) const; Q_INVOKABLE void setStatusMessage(QString msg) const; Q_INVOKABLE void showUserSettingsPage() const; From 092f936fc9efed519b83288eeda386daa9af6a91 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 31 Jul 2021 13:55:56 -0400 Subject: [PATCH 28/45] Fix colors for manual dark theme --- resources/qml/RawMessageDialog.qml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/qml/RawMessageDialog.qml b/resources/qml/RawMessageDialog.qml index 62a5770f..231e2f6d 100644 --- a/resources/qml/RawMessageDialog.qml +++ b/resources/qml/RawMessageDialog.qml @@ -25,6 +25,7 @@ ApplicationWindow { } ScrollView { + anchors.margins: Nheko.paddingMedium anchors.fill: parent palette: Nheko.colors padding: Nheko.paddingMedium @@ -33,8 +34,12 @@ ApplicationWindow { id: rawMessageView font: Nheko.monospaceFont() - palette: Nheko.colors + color: Nheko.colors.text readOnly: true + + background: Rectangle { + color: Nheko.colors.base + } } } From 25e7a985b82bbeff9c3c5b4d15b44f2539768260 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 1 Aug 2021 00:59:46 +0200 Subject: [PATCH 29/45] Add option to only send encrypted messages to verified devices fixes #636 --- src/Cache.cpp | 49 +++++++++++++++-- src/Cache_p.h | 3 +- src/Olm.cpp | 3 +- src/UserSettingsPage.cpp | 113 ++++++++++++++++++++++++--------------- src/UserSettingsPage.h | 7 +++ 5 files changed, 127 insertions(+), 48 deletions(-) diff --git a/src/Cache.cpp b/src/Cache.cpp index 7d0b1a89..291df053 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3542,7 +3542,7 @@ Cache::roomMembers(const std::string &room_id) } std::map> -Cache::getMembersWithKeys(const std::string &room_id) +Cache::getMembersWithKeys(const std::string &room_id, bool verified_only) { std::string_view keys; @@ -3559,10 +3559,51 @@ Cache::getMembersWithKeys(const std::string &room_id) auto res = keysDb.get(txn, user_id, keys); if (res) { - members[std::string(user_id)] = - json::parse(keys).get(); + auto k = json::parse(keys).get(); + if (verified_only) { + auto verif = verificationStatus(std::string(user_id)); + if (verif.user_verified == crypto::Trust::Verified || + !verif.verified_devices.empty()) { + auto keyCopy = k; + keyCopy.device_keys.clear(); + + std::copy_if( + k.device_keys.begin(), + k.device_keys.end(), + std::inserter(keyCopy.device_keys, + keyCopy.device_keys.end()), + [&verif](const auto &key) { + auto curve25519 = key.second.keys.find( + "curve25519:" + key.first); + if (curve25519 == key.second.keys.end()) + return false; + if (auto t = + verif.verified_device_keys.find( + curve25519->second); + t == + verif.verified_device_keys.end() || + t->second != crypto::Trust::Verified) + return false; + + return key.first == + key.second.device_id && + std::find( + verif.verified_devices.begin(), + verif.verified_devices.end(), + key.first) != + verif.verified_devices.end(); + }); + + if (!keyCopy.device_keys.empty()) + members[std::string(user_id)] = + std::move(keyCopy); + } + } else { + members[std::string(user_id)] = std::move(k); + } } else { - members[std::string(user_id)] = {}; + if (!verified_only) + members[std::string(user_id)] = {}; } } cursor.close(); diff --git a/src/Cache_p.h b/src/Cache_p.h index 18b9601f..5d700658 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -48,7 +48,8 @@ public: // user cache stores user keys std::optional userKeys(const std::string &user_id); std::map> getMembersWithKeys( - const std::string &room_id); + const std::string &room_id, + bool verified_only); void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); void markUserKeysOutOfDate(lmdb::txn &txn, diff --git a/src/Olm.cpp b/src/Olm.cpp index e3ca1c34..048a6c0f 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -524,7 +524,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, auto own_user_id = http::client()->user_id().to_string(); - auto members = cache::client()->getMembersWithKeys(room_id); + auto members = cache::client()->getMembersWithKeys( + room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers()); std::map> sendSessionTo; mtx::crypto::OutboundGroupSessionPtr session = nullptr; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index a062780a..ab6ac492 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -90,13 +90,11 @@ UserSettings::load(std::optional profile) decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); - shareKeysWithTrustedUsers_ = - settings.value("user/automatically_share_keys_with_trusted_users", false).toBool(); - mobileMode_ = settings.value("user/mobile_mode", false).toBool(); - emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); - baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); - auto tempPresence = settings.value("user/presence", "").toString().toStdString(); - auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); + mobileMode_ = settings.value("user/mobile_mode", false).toBool(); + emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); + baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); + auto tempPresence = settings.value("user/presence", "").toString().toStdString(); + auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); if (presenceValue < 0) presenceValue = 0; presence_ = static_cast(presenceValue); @@ -123,6 +121,12 @@ UserSettings::load(std::optional profile) userId_ = settings.value(prefix + "auth/user_id", "").toString(); deviceId_ = settings.value(prefix + "auth/device_id", "").toString(); + shareKeysWithTrustedUsers_ = + settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false) + .toBool(); + onlyShareKeysWithVerifiedUsers_ = + settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool(); + disableCertificateValidation_ = settings.value("disable_certificate_validation", false).toBool(); @@ -401,6 +405,17 @@ UserSettings::setUseStunServer(bool useStunServer) save(); } +void +UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys) +{ + if (shareKeys == onlyShareKeysWithVerifiedUsers_) + return; + + onlyShareKeysWithVerifiedUsers_ = shareKeys; + emit onlyShareKeysWithVerifiedUsersChanged(shareKeys); + save(); +} + void UserSettings::setShareKeysWithTrustedUsers(bool shareKeys) { @@ -610,8 +625,6 @@ UserSettings::save() settings.setValue("decrypt_sidebar", decryptSidebar_); settings.setValue("privacy_screen", privacyScreen_); settings.setValue("privacy_screen_timeout", privacyScreenTimeout_); - settings.setValue("automatically_share_keys_with_trusted_users", - shareKeysWithTrustedUsers_); settings.setValue("mobile_mode", mobileMode_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", typingNotifications_); @@ -650,6 +663,11 @@ UserSettings::save() settings.setValue(prefix + "auth/user_id", userId_); settings.setValue(prefix + "auth/device_id", deviceId_); + settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users", + shareKeysWithTrustedUsers_); + settings.setValue(prefix + "user/only_share_keys_with_verified_users", + onlyShareKeysWithVerifiedUsers_); + settings.setValue("disable_certificate_validation", disableCertificateValidation_); settings.sync(); @@ -703,41 +721,43 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); general_->setFont(font); - trayToggle_ = new Toggle{this}; - startInTrayToggle_ = new Toggle{this}; - avatarCircles_ = new Toggle{this}; - decryptSidebar_ = new Toggle(this); - privacyScreen_ = new Toggle{this}; - shareKeysWithTrustedUsers_ = new Toggle(this); - groupViewToggle_ = new Toggle{this}; - timelineButtonsToggle_ = new Toggle{this}; - typingNotifications_ = new Toggle{this}; - messageHoverHighlight_ = new Toggle{this}; - enlargeEmojiOnlyMessages_ = new Toggle{this}; - sortByImportance_ = new Toggle{this}; - readReceipts_ = new Toggle{this}; - markdown_ = new Toggle{this}; - desktopNotifications_ = new Toggle{this}; - alertOnNotification_ = new Toggle{this}; - useStunServer_ = new Toggle{this}; - mobileMode_ = new Toggle{this}; - scaleFactorCombo_ = new QComboBox{this}; - fontSizeCombo_ = new QComboBox{this}; - fontSelectionCombo_ = new QFontComboBox{this}; - emojiFontSelectionCombo_ = new QComboBox{this}; - ringtoneCombo_ = new QComboBox{this}; - microphoneCombo_ = new QComboBox{this}; - cameraCombo_ = new QComboBox{this}; - cameraResolutionCombo_ = new QComboBox{this}; - cameraFrameRateCombo_ = new QComboBox{this}; - timelineMaxWidthSpin_ = new QSpinBox{this}; - privacyScreenTimeout_ = new QSpinBox{this}; + trayToggle_ = new Toggle{this}; + startInTrayToggle_ = new Toggle{this}; + avatarCircles_ = new Toggle{this}; + decryptSidebar_ = new Toggle(this); + privacyScreen_ = new Toggle{this}; + onlyShareKeysWithVerifiedUsers_ = new Toggle(this); + shareKeysWithTrustedUsers_ = new Toggle(this); + groupViewToggle_ = new Toggle{this}; + timelineButtonsToggle_ = new Toggle{this}; + typingNotifications_ = new Toggle{this}; + messageHoverHighlight_ = new Toggle{this}; + enlargeEmojiOnlyMessages_ = new Toggle{this}; + sortByImportance_ = new Toggle{this}; + readReceipts_ = new Toggle{this}; + markdown_ = new Toggle{this}; + desktopNotifications_ = new Toggle{this}; + alertOnNotification_ = new Toggle{this}; + useStunServer_ = new Toggle{this}; + mobileMode_ = new Toggle{this}; + scaleFactorCombo_ = new QComboBox{this}; + fontSizeCombo_ = new QComboBox{this}; + fontSelectionCombo_ = new QFontComboBox{this}; + emojiFontSelectionCombo_ = new QComboBox{this}; + ringtoneCombo_ = new QComboBox{this}; + microphoneCombo_ = new QComboBox{this}; + cameraCombo_ = new QComboBox{this}; + cameraResolutionCombo_ = new QComboBox{this}; + cameraFrameRateCombo_ = new QComboBox{this}; + timelineMaxWidthSpin_ = new QSpinBox{this}; + privacyScreenTimeout_ = new QSpinBox{this}; trayToggle_->setChecked(settings_->tray()); startInTrayToggle_->setChecked(settings_->startInTray()); avatarCircles_->setChecked(settings_->avatarCircles()); decryptSidebar_->setChecked(settings_->decryptSidebar()); privacyScreen_->setChecked(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers()); shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers()); groupViewToggle_->setChecked(settings_->groupView()); timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline()); @@ -1008,10 +1028,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge formLayout_->addRow(new HorizontalLine{this}); boxWrap(tr("Device ID"), deviceIdValue_); boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); - boxWrap( - tr("Share keys with verified users and devices"), - shareKeysWithTrustedUsers_, - tr("Automatically replies to key requests from other users, if they are verified.")); + boxWrap(tr("Send encrypted messages to verified users only"), + onlyShareKeysWithVerifiedUsers_, + tr("Requires a user to be verified to send encrypted messages to them. This " + "improves safety but makes E2EE more tedious.")); + boxWrap(tr("Share keys with verified users and devices"), + shareKeysWithTrustedUsers_, + tr("Automatically replies to key requests from other users, if they are verified, " + "even if that device shouldn't have access to those keys otherwise.")); formLayout_->addRow(new HorizontalLine{this}); formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout); @@ -1179,6 +1203,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge } }); + connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setOnlyShareKeysWithVerifiedUsers(enabled); + }); + connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) { settings_->setShareKeysWithTrustedUsers(enabled); }); @@ -1271,6 +1299,7 @@ UserSettingsPage::showEvent(QShowEvent *) groupViewToggle_->setState(settings_->groupView()); decryptSidebar_->setState(settings_->decryptSidebar()); privacyScreen_->setState(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers()); shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers()); avatarCircles_->setState(settings_->avatarCircles()); typingNotifications_->setState(settings_->typingNotifications()); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index acb08569..096aab81 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -88,6 +88,8 @@ class UserSettings : public QObject setScreenShareHideCursor NOTIFY screenShareHideCursorChanged) Q_PROPERTY( bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) + Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE + setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged) Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged) Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged) @@ -152,6 +154,7 @@ public: void setScreenShareRemoteVideo(bool state); void setScreenShareHideCursor(bool state); void setUseStunServer(bool state); + void setOnlyShareKeysWithVerifiedUsers(bool state); void setShareKeysWithTrustedUsers(bool state); void setProfile(QString profile); void setUserId(QString userId); @@ -208,6 +211,7 @@ public: bool screenShareHideCursor() const { return screenShareHideCursor_; } bool useStunServer() const { return useStunServer_; } bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; } + bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; } QString profile() const { return profile_; } QString userId() const { return userId_; } QString accessToken() const { return accessToken_; } @@ -252,6 +256,7 @@ signals: void screenShareRemoteVideoChanged(bool state); void screenShareHideCursorChanged(bool state); void useStunServerChanged(bool state); + void onlyShareKeysWithVerifiedUsersChanged(bool state); void shareKeysWithTrustedUsersChanged(bool state); void profileChanged(QString profile); void userIdChanged(QString userId); @@ -284,6 +289,7 @@ private: bool privacyScreen_; int privacyScreenTimeout_; bool shareKeysWithTrustedUsers_; + bool onlyShareKeysWithVerifiedUsers_; bool mobileMode_; int timelineMaxWidth_; int roomListWidth_; @@ -372,6 +378,7 @@ private: Toggle *privacyScreen_; QSpinBox *privacyScreenTimeout_; Toggle *shareKeysWithTrustedUsers_; + Toggle *onlyShareKeysWithVerifiedUsers_; Toggle *mobileMode_; QLabel *deviceFingerprintValue_; QLabel *deviceIdValue_; From 041d8fb56c435a3a5f5af9ff304f54deb5883c9b Mon Sep 17 00:00:00 2001 From: Callum Brown Date: Wed, 21 Jul 2021 11:55:41 +0100 Subject: [PATCH 30/45] Reorganise src/RegisterPage.cpp --- src/RegisterPage.cpp | 550 ++++++++++++++++++++----------------------- src/RegisterPage.h | 39 ++- 2 files changed, 286 insertions(+), 303 deletions(-) diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp index 1588d07d..1d529f54 100644 --- a/src/RegisterPage.cpp +++ b/src/RegisterPage.cpp @@ -12,6 +12,7 @@ #include #include +#include #include "Config.h" #include "Logging.h" @@ -93,6 +94,7 @@ RegisterPage::RegisterPage(QWidget *parent) server_input_ = new TextField(); server_input_->setLabel(tr("Homeserver")); + server_input_->setRegexp(QRegularExpression("[a-z0-9.-]+")); server_input_->setToolTip( tr("A server that allows registration. Since matrix is decentralized, you need to first " "find a server you can register on or host your own.")); @@ -145,178 +147,39 @@ RegisterPage::RegisterPage(QWidget *parent) top_layout_->addLayout(button_layout_); top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); top_layout_->addStretch(1); - - connect( - this, - &RegisterPage::versionErrorCb, - this, - [this](const QString &msg) { - error_server_label_->show(); - server_input_->setValid(false); - showError(error_server_label_, msg); - }, - Qt::QueuedConnection); + setLayout(top_layout_); connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkFields); + connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername); connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkFields); + connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword); connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect( - password_confirmation_, &TextField::editingFinished, this, &RegisterPage::checkFields); + connect(password_confirmation_, + &TextField::editingFinished, + this, + &RegisterPage::checkPasswordConfirmation); connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkFields); - connect(this, &RegisterPage::registerErrorCb, this, [this](const QString &msg) { - showError(msg); - }); - connect( - this, - &RegisterPage::registrationFlow, - this, - [this](const std::string &user, - const std::string &pass, - const mtx::user_interactive::Unauthorized &unauthorized) { - auto completed_stages = unauthorized.completed; - auto flows = unauthorized.flows; - auto session = unauthorized.session.empty() ? http::client()->generate_txn_id() - : unauthorized.session; - - nhlog::ui()->info("Completed stages: {}", completed_stages.size()); - - if (!completed_stages.empty()) - flows.erase(std::remove_if( - flows.begin(), - flows.end(), - [completed_stages](auto flow) { - if (completed_stages.size() > flow.stages.size()) - return true; - for (size_t f = 0; f < completed_stages.size(); f++) - if (completed_stages[f] != flow.stages[f]) - return true; - return false; - }), - flows.end()); - - if (flows.empty()) { - nhlog::net()->error("No available registration flows!"); - emit registerErrorCb(tr("No supported registration flows!")); - return; - } - - auto current_stage = flows.front().stages.at(completed_stages.size()); - - if (current_stage == mtx::user_interactive::auth_types::recaptcha) { - auto captchaDialog = - new dialogs::ReCaptcha(QString::fromStdString(session), this); - - connect(captchaDialog, - &dialogs::ReCaptcha::confirmation, - this, - [this, user, pass, session, captchaDialog]() { - captchaDialog->close(); - captchaDialog->deleteLater(); - - emit registerAuth( - user, - pass, - mtx::user_interactive::Auth{ - session, mtx::user_interactive::auth::Fallback{}}); - }); - connect(captchaDialog, - &dialogs::ReCaptcha::cancel, - this, - &RegisterPage::errorOccurred); - - QTimer::singleShot( - 1000, this, [captchaDialog]() { captchaDialog->show(); }); - } else if (current_stage == mtx::user_interactive::auth_types::dummy) { - emit registerAuth(user, - pass, - mtx::user_interactive::Auth{ - session, mtx::user_interactive::auth::Dummy{}}); - } else { - // use fallback - auto dialog = - new dialogs::FallbackAuth(QString::fromStdString(current_stage), - QString::fromStdString(session), - this); - - connect(dialog, - &dialogs::FallbackAuth::confirmation, - this, - [this, user, pass, session, dialog]() { - dialog->close(); - dialog->deleteLater(); - - emit registerAuth( - user, - pass, - mtx::user_interactive::Auth{ - session, mtx::user_interactive::auth::Fallback{}}); - }); - connect(dialog, - &dialogs::FallbackAuth::cancel, - this, - &RegisterPage::errorOccurred); - - dialog->show(); - } - }); + connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer); connect( this, - &RegisterPage::registerAuth, + &RegisterPage::serverError, this, - [this](const std::string &user, - const std::string &pass, - const mtx::user_interactive::Auth &auth) { - http::client()->registration( - user, - pass, - auth, - [this, user, pass](const mtx::responses::Register &res, - mtx::http::RequestErr err) { - if (!err) { - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); - http::client()->set_device_id(res.device_id); + [this](const QString &msg) { + server_input_->setValid(false); + showError(error_server_label_, msg); + }, + Qt::QueuedConnection); - emit registerOk(); - return; - } - - // The server requires registration flows. - if (err->status_code == 401) { - if (err->matrix_error.unauthorized.flows.empty()) { - nhlog::net()->warn( - "failed to retrieve registration flows: ({}) " - "{}", - static_cast(err->status_code), - err->matrix_error.error); - emit registerErrorCb( - QString::fromStdString(err->matrix_error.error)); - return; - } - - emit registrationFlow( - user, pass, err->matrix_error.unauthorized); - return; - } - - nhlog::net()->warn("failed to register: status_code ({}), " - "matrix_error: ({}), parser error ({})", - static_cast(err->status_code), - err->matrix_error.error, - err->parse_error); - - emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); - }); - }); - - setLayout(top_layout_); + connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup); + connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck); + connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration); + connect(this, &RegisterPage::UIA, this, &RegisterPage::doUIA); + connect( + this, &RegisterPage::registrationWithAuth, this, &RegisterPage::doRegistrationWithAuth); } void @@ -345,191 +208,298 @@ RegisterPage::showError(QLabel *label, const QString &msg) int height = rect.height(); label->setFixedHeight((int)qCeil(width / 200.0) * height); label->setText(msg); + label->show(); } bool RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg) { if (t_field->isValid()) { - label->setText(""); label->hide(); return true; } else { - label->show(); showError(label, msg); return false; } } bool -RegisterPage::checkFields() +RegisterPage::checkUsername() { - error_label_->setText(""); - error_username_label_->setText(""); - error_password_label_->setText(""); - error_password_confirmation_label_->setText(""); - error_server_label_->setText(""); + return checkOneField(error_username_label_, + username_input_, + tr("The username must not be empty, and must contain only the " + "characters a-z, 0-9, ., _, =, -, and /.")); +} - error_username_label_->hide(); - error_password_label_->hide(); - error_password_confirmation_label_->hide(); - error_server_label_->hide(); +bool +RegisterPage::checkPassword() +{ + return checkOneField( + error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)")); +} - password_confirmation_->setValid(true); - server_input_->setValid(true); - - bool all_fields_good = true; - if (username_input_->isModified() && - !checkOneField(error_username_label_, - username_input_, - tr("The username must not be empty, and must contain only the " - "characters a-z, 0-9, ., _, =, -, and /."))) { - all_fields_good = false; - } else if (password_input_->isModified() && - !checkOneField(error_password_label_, - password_input_, - tr("Password is not long enough (min 8 chars)"))) { - all_fields_good = false; - } else if (password_confirmation_->isModified() && - password_input_->text() != password_confirmation_->text()) { - error_password_confirmation_label_->show(); +bool +RegisterPage::checkPasswordConfirmation() +{ + if (password_input_->text() == password_confirmation_->text()) { + error_password_confirmation_label_->hide(); + password_confirmation_->setValid(true); + return true; + } else { showError(error_password_confirmation_label_, tr("Passwords don't match")); password_confirmation_->setValid(false); - all_fields_good = false; - } else if (server_input_->isModified() && - (!server_input_->hasAcceptableInput() || server_input_->text().isEmpty())) { - error_server_label_->show(); - showError(error_server_label_, tr("Invalid server name")); - server_input_->setValid(false); - all_fields_good = false; + return false; } - if (!username_input_->isModified() || !password_input_->isModified() || - !password_confirmation_->isModified() || !server_input_->isModified()) { - all_fields_good = false; - } - return all_fields_good; +} + +bool +RegisterPage::checkServer() +{ + // This doesn't check that the server is reachable, + // just that the input is not obviously wrong. + return checkOneField(error_server_label_, server_input_, tr("Invalid server name")); } void RegisterPage::onRegisterButtonClicked() { - if (!checkFields()) { - showError(error_label_, - tr("One or more fields have invalid inputs. Please correct those issues " - "and try again.")); - return; - } else { - auto username = username_input_->text().toStdString(); - auto password = password_input_->text().toStdString(); - auto server = server_input_->text().toStdString(); + if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) { + auto server = server_input_->text().toStdString(); http::client()->set_server(server); http::client()->verify_certificates( !UserSettings::instance()->disableCertificateValidation()); - http::client()->well_known( - [this, username, password](const mtx::responses::WellKnown &res, - mtx::http::RequestErr err) { - if (err) { - if (err->status_code == 404) { - nhlog::net()->info("Autodiscovery: No .well-known."); - checkVersionAndRegister(username, password); - return; - } + // This starts a chain of `emit`s which ends up doing the + // registration. Signals are used rather than normal function + // calls so that the dialogs used in UIA work correctly. + // + // The sequence of events looks something like this: + // + // dowellKnownLookup + // v + // doVersionsCheck + // v + // doRegistration + // v + // doUIA <-----------------+ + // v | More auth required + // doRegistrationWithAuth -+ + // | Success + // v + // registering - if (!err->parse_error.empty()) { - emit versionErrorCb(tr( - "Autodiscovery failed. Received malformed response.")); - nhlog::net()->error( - "Autodiscovery failed. Received malformed response."); - return; - } - - emit versionErrorCb(tr("Autodiscovery failed. Unknown error when " - "requesting .well-known.")); - nhlog::net()->error("Autodiscovery failed. Unknown error when " - "requesting .well-known. {} {}", - err->status_code, - err->error_code); - return; - } - - nhlog::net()->info("Autodiscovery: Discovered '" + - res.homeserver.base_url + "'"); - http::client()->set_server(res.homeserver.base_url); - checkVersionAndRegister(username, password); - }); + emit wellKnownLookup(); emit registering(); } } void -RegisterPage::checkVersionAndRegister(const std::string &username, const std::string &password) +RegisterPage::doWellKnownLookup() { - http::client()->versions( - [this, username, password](const mtx::responses::Versions &, mtx::http::RequestErr err) { + http::client()->well_known( + [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) { if (err) { if (err->status_code == 404) { - emit versionErrorCb(tr("The required endpoints were not found. " - "Possibly not a Matrix server.")); + nhlog::net()->info("Autodiscovery: No .well-known."); + // Check that the homeserver can be reached + emit versionsCheck(); return; } if (!err->parse_error.empty()) { - emit versionErrorCb(tr("Received malformed response. Make sure " - "the homeserver domain is valid.")); + emit serverError( + tr("Autodiscovery failed. Received malformed response.")); + nhlog::net()->error( + "Autodiscovery failed. Received malformed response."); return; } - emit versionErrorCb(tr( - "An unknown error occured. Make sure the homeserver domain is valid.")); + emit serverError(tr("Autodiscovery failed. Unknown error when " + "requesting .well-known.")); + nhlog::net()->error("Autodiscovery failed. Unknown error when " + "requesting .well-known. {} {}", + err->status_code, + err->error_code); return; } - http::client()->registration( - username, - password, - [this, username, password](const mtx::responses::Register &res, - mtx::http::RequestErr err) { - if (!err) { - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); - - emit registerOk(); - return; - } - - // The server requires registration flows. - if (err->status_code == 401) { - if (err->matrix_error.unauthorized.flows.empty()) { - nhlog::net()->warn( - "failed to retrieve registration flows1: ({}) " - "{}", - static_cast(err->status_code), - err->matrix_error.error); - emit errorOccurred(); - emit registerErrorCb( - QString::fromStdString(err->matrix_error.error)); - return; - } - - emit registrationFlow( - username, password, err->matrix_error.unauthorized); - return; - } - - nhlog::net()->error( - "failed to register: status_code ({}), matrix_error({})", - static_cast(err->status_code), - err->matrix_error.error); - - emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); - emit errorOccurred(); - }); + nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'"); + http::client()->set_server(res.homeserver.base_url); + // Check that the homeserver can be reached + emit versionsCheck(); }); } +void +RegisterPage::doVersionsCheck() +{ + // Make a request to /_matrix/client/versions to check the address + // given is a Matrix homeserver. + http::client()->versions( + [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + emit serverError( + tr("The required endpoints were not found. Possibly " + "not a Matrix server.")); + return; + } + + if (!err->parse_error.empty()) { + emit serverError( + tr("Received malformed response. Make sure the homeserver " + "domain is valid.")); + return; + } + + emit serverError(tr("An unknown error occured. Make sure the " + "homeserver domain is valid.")); + return; + } + + // Attempt registration without an `auth` dict + emit registration(); + }); +} + +void +RegisterPage::doRegistration() +{ + // These inputs should still be alright, but check just in case + if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { + auto username = username_input_->text().toStdString(); + auto password = password_input_->text().toStdString(); + http::client()->registration(username, password, registrationCb()); + } +} + +void +RegisterPage::doRegistrationWithAuth(const mtx::user_interactive::Auth &auth) +{ + // These inputs should still be alright, but check just in case + if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { + auto username = username_input_->text().toStdString(); + auto password = password_input_->text().toStdString(); + http::client()->registration(username, password, auth, registrationCb()); + } +} + +mtx::http::Callback +RegisterPage::registrationCb() +{ + // Return a function to be used as the callback when an attempt at + // registration is made. + return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) { + if (!err) { + http::client()->set_user(res.user_id); + http::client()->set_access_token(res.access_token); + emit registerOk(); + return; + } + + // The server requires registration flows. + if (err->status_code == 401) { + if (err->matrix_error.unauthorized.flows.empty()) { + nhlog::net()->warn("failed to retrieve registration flows: " + "status_code({}), matrix_error({}) ", + static_cast(err->status_code), + err->matrix_error.error); + showError(QString::fromStdString(err->matrix_error.error)); + return; + } + + // Attempt to complete a UIA stage + emit UIA(err->matrix_error.unauthorized); + return; + } + + nhlog::net()->error("failed to register: status_code ({}), matrix_error({})", + static_cast(err->status_code), + err->matrix_error.error); + + showError(QString::fromStdString(err->matrix_error.error)); + }; +} + +void +RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized) +{ + auto completed_stages = unauthorized.completed; + auto flows = unauthorized.flows; + auto session = + unauthorized.session.empty() ? http::client()->generate_txn_id() : unauthorized.session; + + nhlog::ui()->info("Completed stages: {}", completed_stages.size()); + + if (!completed_stages.empty()) { + // Get rid of all flows which don't start with the sequence of + // stages that have already been completed. + flows.erase( + std::remove_if(flows.begin(), + flows.end(), + [completed_stages](auto flow) { + if (completed_stages.size() > flow.stages.size()) + return true; + for (size_t f = 0; f < completed_stages.size(); f++) + if (completed_stages[f] != flow.stages[f]) + return true; + return false; + }), + flows.end()); + } + + if (flows.empty()) { + nhlog::ui()->error("No available registration flows!"); + showError(tr("No supported registration flows!")); + return; + } + + auto current_stage = flows.front().stages.at(completed_stages.size()); + + if (current_stage == mtx::user_interactive::auth_types::recaptcha) { + auto captchaDialog = new dialogs::ReCaptcha(QString::fromStdString(session), this); + + connect(captchaDialog, + &dialogs::ReCaptcha::confirmation, + this, + [this, session, captchaDialog]() { + captchaDialog->close(); + captchaDialog->deleteLater(); + doRegistrationWithAuth(mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::Fallback{}}); + }); + + connect( + captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred); + + QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); }); + + } else if (current_stage == mtx::user_interactive::auth_types::dummy) { + doRegistrationWithAuth( + mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}}); + + } else { + // use fallback + auto dialog = new dialogs::FallbackAuth( + QString::fromStdString(current_stage), QString::fromStdString(session), this); + + connect( + dialog, &dialogs::FallbackAuth::confirmation, this, [this, session, dialog]() { + dialog->close(); + dialog->deleteLater(); + emit registrationWithAuth(mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::Fallback{}}); + }); + + connect(dialog, &dialogs::FallbackAuth::cancel, this, &RegisterPage::errorOccurred); + + dialog->show(); + } +} + void RegisterPage::paintEvent(QPaintEvent *) { diff --git a/src/RegisterPage.h b/src/RegisterPage.h index 0e4a45d0..44128939 100644 --- a/src/RegisterPage.h +++ b/src/RegisterPage.h @@ -10,6 +10,7 @@ #include #include +#include class FlatButton; class RaisedButton; @@ -33,30 +34,40 @@ signals: void errorOccurred(); //! Used to trigger the corresponding slot outside of the main thread. - void versionErrorCb(const QString &err); + void serverError(const QString &err); + + void wellKnownLookup(); + void versionsCheck(); + void registration(); + void UIA(const mtx::user_interactive::Unauthorized &unauthorized); + void registrationWithAuth(const mtx::user_interactive::Auth &auth); void registering(); void registerOk(); - void registerErrorCb(const QString &msg); - void registrationFlow(const std::string &user, - const std::string &pass, - const mtx::user_interactive::Unauthorized &unauthorized); - void registerAuth(const std::string &user, - const std::string &pass, - const mtx::user_interactive::Auth &auth); private slots: void onBackButtonClicked(); void onRegisterButtonClicked(); +private: // function for showing different errors void showError(const QString &msg); - -private: - bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg); - bool checkFields(); void showError(QLabel *label, const QString &msg); - void checkVersionAndRegister(const std::string &username, const std::string &password); + + bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg); + bool checkUsername(); + bool checkPassword(); + bool checkPasswordConfirmation(); + bool checkServer(); + + void doWellKnownLookup(); + void doVersionsCheck(); + void doRegistration(); + void doUIA(const mtx::user_interactive::Unauthorized &unauthorized); + void doRegistrationWithAuth(const mtx::user_interactive::Auth &auth); + mtx::http::Callback registrationCb(); + void completeUiaStage(const mtx::user_interactive::Unauthorized &unauthorized); + QVBoxLayout *top_layout_; QHBoxLayout *back_layout_; @@ -69,6 +80,7 @@ private: QLabel *error_password_label_; QLabel *error_password_confirmation_label_; QLabel *error_server_label_; + QLabel *error_registration_token_label_; FlatButton *back_button_; RaisedButton *register_button_; @@ -81,4 +93,5 @@ private: TextField *password_input_; TextField *password_confirmation_; TextField *server_input_; + TextField *registration_token_input_; }; From 87e81498b73d59e8173953079ca5d0d73c5c302f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 4 Aug 2021 02:27:50 +0200 Subject: [PATCH 31/45] Fix window placement on wayland and add close buttons We explicitly set a parent. We can't assign to ApplicationWindow.transientParent though, only to Window.transientParent, so we just call setTransientParent in C++. --- resources/qml/InviteDialog.qml | 4 ++-- resources/qml/MessageInput.qml | 2 +- resources/qml/MessageView.qml | 2 +- resources/qml/RawMessageDialog.qml | 7 ++++--- resources/qml/ReadReceipts.qml | 5 ++--- resources/qml/RoomMembers.qml | 6 +++--- resources/qml/RoomSettings.qml | 7 +++---- resources/qml/Root.qml | 6 +++--- resources/qml/TimelineRow.qml | 2 +- resources/qml/TimelineView.qml | 2 +- resources/qml/UserProfile.qml | 14 ++++++++------ resources/qml/delegates/Reply.qml | 2 +- .../qml/device-verification/DeviceVerification.qml | 7 +++---- resources/qml/dialogs/ImagePackSettingsDialog.qml | 11 ++++++++--- resources/qml/dialogs/InputDialog.qml | 1 + src/ui/NhekoGlobalObject.cpp | 7 +++++++ src/ui/NhekoGlobalObject.h | 3 +++ 17 files changed, 52 insertions(+), 36 deletions(-) diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index 50287ad5..2c0e15a7 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -30,12 +30,12 @@ ApplicationWindow { } title: qsTr("Invite users to %1").arg(plainRoomName) - x: MainWindow.x + (MainWindow.width / 2) - (width / 2) - y: MainWindow.y + (MainWindow.height / 2) - (height / 2) height: 380 width: 340 palette: Nheko.colors color: Nheko.colors.window + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(inviteDialogRoot) Shortcut { sequence: "Ctrl+Enter" diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 8bc8ac62..7fb09684 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -7,7 +7,7 @@ import "./voip" import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.2 +import QtQuick.Window 2.13 import im.nheko 1.0 Rectangle { diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 9ba5e2d0..f3e15d84 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -10,7 +10,7 @@ import QtGraphicalEffects 1.0 import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.2 +import QtQuick.Window 2.13 import im.nheko 1.0 ScrollView { diff --git a/resources/qml/RawMessageDialog.qml b/resources/qml/RawMessageDialog.qml index 231e2f6d..e2a476cd 100644 --- a/resources/qml/RawMessageDialog.qml +++ b/resources/qml/RawMessageDialog.qml @@ -11,13 +11,12 @@ ApplicationWindow { property alias rawMessage: rawMessageView.text - x: MainWindow.x + (MainWindow.width / 2) - (width / 2) - y: MainWindow.y + (MainWindow.height / 2) - (height / 2) height: 420 width: 420 palette: Nheko.colors color: Nheko.colors.window - flags: Qt.Tool | Qt.WindowStaysOnTopHint + flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(rawMessageRoot) Shortcut { sequence: StandardKey.Cancel @@ -40,6 +39,7 @@ ApplicationWindow { background: Rectangle { color: Nheko.colors.base } + } } @@ -48,4 +48,5 @@ ApplicationWindow { standardButtons: DialogButtonBox.Ok onAccepted: rawMessageRoot.close() } + } diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml index 8869d813..9adbfd5c 100644 --- a/resources/qml/ReadReceipts.qml +++ b/resources/qml/ReadReceipts.qml @@ -13,15 +13,14 @@ ApplicationWindow { property ReadReceiptsProxy readReceipts property Room room - 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 - flags: Qt.Dialog + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(readReceiptsRoot) Shortcut { sequence: StandardKey.Cancel diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml index 641a08be..447e6fd1 100644 --- a/resources/qml/RoomMembers.qml +++ b/resources/qml/RoomMembers.qml @@ -6,7 +6,7 @@ import "./ui" import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 -import QtQuick.Window 2.12 +import QtQuick.Window 2.13 import im.nheko 1.0 ApplicationWindow { @@ -15,13 +15,13 @@ ApplicationWindow { property MemberList members title: qsTr("Members of %1").arg(members.roomName) - x: MainWindow.x + (MainWindow.width / 2) - (width / 2) - y: MainWindow.y + (MainWindow.height / 2) - (height / 2) height: 650 width: 420 minimumHeight: 420 palette: Nheko.colors color: Nheko.colors.window + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(roomMembersRoot) Shortcut { sequence: StandardKey.Cancel diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index b8e527a5..6ba080c4 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -7,7 +7,7 @@ import Qt.labs.platform 1.1 as Platform import QtQuick 2.15 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.3 +import QtQuick.Window 2.13 import im.nheko 1.0 ApplicationWindow { @@ -15,14 +15,13 @@ ApplicationWindow { property var roomSettings - x: MainWindow.x + (MainWindow.width / 2) - (width / 2) - y: MainWindow.y + (MainWindow.height / 2) - (height / 2) minimumWidth: 420 minimumHeight: 650 palette: Nheko.colors color: Nheko.colors.window modality: Qt.NonModal - flags: Qt.Dialog + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(roomSettingsDialog) title: qsTr("Room Settings") Shortcut { diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 70cfbda5..b229acda 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -9,10 +9,10 @@ import "./emoji" import "./voip" import Qt.labs.platform 1.1 as Platform import QtGraphicalEffects 1.0 -import QtQuick 2.9 -import QtQuick.Controls 2.5 +import QtQuick 2.15 +import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 -import QtQuick.Window 2.2 +import QtQuick.Window 2.15 import im.nheko 1.0 import im.nheko.EmojiModel 1.0 diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 755ab503..6345f44c 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -7,7 +7,7 @@ import "./emoji" import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.2 +import QtQuick.Window 2.13 import im.nheko 1.0 Item { diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index e4036eb7..6fc9d51b 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -13,7 +13,7 @@ import QtGraphicalEffects 1.0 import QtQuick 2.9 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.3 -import QtQuick.Window 2.2 +import QtQuick.Window 2.13 import im.nheko 1.0 import im.nheko.EmojiModel 1.0 diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml index d138060b..767d2317 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml @@ -4,19 +4,20 @@ import "./device-verification" import "./ui" -import QtQuick 2.9 -import QtQuick.Controls 2.3 +import QtQuick 2.15 +import QtQuick.Controls 2.15 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.3 +import QtQuick.Window 2.13 import im.nheko 1.0 ApplicationWindow { + // this does not work in ApplicationWindow, just in Window + //transientParent: Nheko.mainwindow() + id: userProfileDialog property var profile - x: MainWindow.x + (MainWindow.width / 2) - (width / 2) - y: MainWindow.y + (MainWindow.height / 2) - (height / 2) height: 650 width: 420 minimumHeight: 420 @@ -24,7 +25,8 @@ ApplicationWindow { color: Nheko.colors.window title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile") modality: Qt.NonModal - flags: Qt.Dialog + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(userProfileDialog) Shortcut { sequence: StandardKey.Cancel diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 75e3d617..3e02a940 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -5,7 +5,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtQuick.Window 2.2 +import QtQuick.Window 2.13 import im.nheko 1.0 Item { diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml index e2c66c5a..8e0271d6 100644 --- a/resources/qml/device-verification/DeviceVerification.qml +++ b/resources/qml/device-verification/DeviceVerification.qml @@ -4,7 +4,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 -import QtQuick.Window 2.10 +import QtQuick.Window 2.13 import im.nheko 1.0 ApplicationWindow { @@ -14,13 +14,12 @@ ApplicationWindow { onClosing: TimelineManager.removeVerificationFlow(flow) title: stack.currentItem.title - flags: Qt.Dialog modality: Qt.NonModal palette: Nheko.colors height: stack.implicitHeight width: stack.implicitWidth - x: MainWindow.x + (MainWindow.width / 2) - (width / 2) - y: MainWindow.y + (MainWindow.height / 2) - (height / 2) + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(dialog) StackView { id: stack diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml index c4b4a885..3d830bf7 100644 --- a/resources/qml/dialogs/ImagePackSettingsDialog.qml +++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml @@ -20,14 +20,13 @@ ApplicationWindow { readonly property int stickerDimPad: 128 + Nheko.paddingSmall title: qsTr("Image pack settings") - x: MainWindow.x + (MainWindow.width / 2) - (width / 2) - y: MainWindow.y + (MainWindow.height / 2) - (height / 2) height: 400 width: 600 palette: Nheko.colors color: Nheko.colors.base modality: Qt.NonModal - flags: Qt.Dialog + flags: Qt.Dialog | Qt.WindowCloseButtonHint + Component.onCompleted: Nheko.reparent(win) AdaptiveLayout { id: adaptiveView @@ -202,6 +201,12 @@ ApplicationWindow { color: Nheko.colors.window ColumnLayout { + //Button { + // Layout.alignment: Qt.AlignHCenter + // text: qsTr("Edit") + // enabled: currentPack.canEdit + //} + id: packinfo property string packName: currentPack ? currentPack.packname : "" diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml index 134b78a3..e0f17851 100644 --- a/resources/qml/dialogs/InputDialog.qml +++ b/resources/qml/dialogs/InputDialog.qml @@ -16,6 +16,7 @@ ApplicationWindow { modality: Qt.NonModal flags: Qt.Dialog + Component.onCompleted: Nheko.reparent(inputDialog) width: 350 height: fontMetrics.lineSpacing * 7 diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp index fea10839..9e0d706b 100644 --- a/src/ui/NhekoGlobalObject.cpp +++ b/src/ui/NhekoGlobalObject.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "Cache_p.h" #include "ChatPage.h" @@ -140,3 +141,9 @@ Nheko::openJoinRoomDialog() const MainWindow::instance()->openJoinRoomDialog( [](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); }); } + +void +Nheko::reparent(QWindow *win) const +{ + win->setTransientParent(MainWindow::instance()->windowHandle()); +} diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index cfe982c5..d4d119dc 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -11,6 +11,8 @@ #include "Theme.h" #include "UserProfile.h" +class QWindow; + class Nheko : public QObject { Q_OBJECT @@ -49,6 +51,7 @@ public: Q_INVOKABLE void openLogoutDialog() const; Q_INVOKABLE void openCreateRoomDialog() const; Q_INVOKABLE void openJoinRoomDialog() const; + Q_INVOKABLE void reparent(QWindow *win) const; public slots: void updateUserProfile(); From 571ae3d51b70d4e71b54fae929ad1e41a5fac06c Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Wed, 4 Aug 2021 18:00:37 -0400 Subject: [PATCH 32/45] Disable brew in macos CI --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7ff92c17..d551f2a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,9 +52,9 @@ build-macos: stage: build tags: [macos] before_script: - - brew update - - brew reinstall --force python3 - - brew bundle --file=./.ci/macos/Brewfile --force --cleanup + #- brew update + #- brew reinstall --force python3 + #- brew bundle --file=./.ci/macos/Brewfile --force --cleanup - pip3 install dmgbuild - rm -rf ../.hunter && mv .hunter ../.hunter || true script: From bb6a57644c46dec6d4fc9f8e30839677ae278fb2 Mon Sep 17 00:00:00 2001 From: Callum Brown Date: Thu, 5 Aug 2021 16:12:36 +0100 Subject: [PATCH 33/45] Make things private slots --- src/RegisterPage.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/RegisterPage.h b/src/RegisterPage.h index 44128939..42ea00cb 100644 --- a/src/RegisterPage.h +++ b/src/RegisterPage.h @@ -49,7 +49,6 @@ private slots: void onBackButtonClicked(); void onRegisterButtonClicked(); -private: // function for showing different errors void showError(const QString &msg); void showError(QLabel *label, const QString &msg); @@ -66,8 +65,8 @@ private: void doUIA(const mtx::user_interactive::Unauthorized &unauthorized); void doRegistrationWithAuth(const mtx::user_interactive::Auth &auth); mtx::http::Callback registrationCb(); - void completeUiaStage(const mtx::user_interactive::Unauthorized &unauthorized); +private: QVBoxLayout *top_layout_; QHBoxLayout *back_layout_; From bd31726f2ff103dda7c84b8a6e801d15404ac96b Mon Sep 17 00:00:00 2001 From: Callum Brown Date: Thu, 5 Aug 2021 16:41:40 +0100 Subject: [PATCH 34/45] Allow all characters when checking server input So IDNs are not rejected. Invalid server names will be caught later. --- src/RegisterPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp index 1d529f54..bae24df0 100644 --- a/src/RegisterPage.cpp +++ b/src/RegisterPage.cpp @@ -94,7 +94,7 @@ RegisterPage::RegisterPage(QWidget *parent) server_input_ = new TextField(); server_input_->setLabel(tr("Homeserver")); - server_input_->setRegexp(QRegularExpression("[a-z0-9.-]+")); + server_input_->setRegexp(QRegularExpression(".+")); server_input_->setToolTip( tr("A server that allows registration. Since matrix is decentralized, you need to first " "find a server you can register on or host your own.")); From f7d1d1b9416248bb75bbacc3fa14356a3acc9f24 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Tue, 3 Aug 2021 07:20:36 -0400 Subject: [PATCH 35/45] Open profile when clicking avatar --- resources/qml/RoomList.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index cbc65fc0..98532606 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -439,6 +439,7 @@ Page { url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/") displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : "" userid: userInfoGrid.profile ? userInfoGrid.profile.userid : "" + enabled: false } ColumnLayout { From b156dd51cb8a393ab83af936767e95ce8379e157 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Thu, 5 Aug 2021 22:22:47 -0400 Subject: [PATCH 36/45] Update qt5 path after brew changes --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d551f2a3..e24c5c85 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -58,8 +58,8 @@ build-macos: - pip3 install dmgbuild - rm -rf ../.hunter && mv .hunter ../.hunter || true script: - - export PATH=/usr/local/opt/qt/bin/:${PATH} - - export CMAKE_PREFIX_PATH=/usr/local/opt/qt5 + - export PATH=/usr/local/opt/qt@5/bin/:${PATH} + - export CMAKE_PREFIX_PATH=/usr/local/opt/qt@5 - cmake -GNinja -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=.deps/usr From a57a15a2e07da8cc07bc12e828b7c636efe36cbc Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 6 Aug 2021 01:45:47 +0200 Subject: [PATCH 37/45] Basic sticker pack editor --- CMakeLists.txt | 2 +- io.github.NhekoReborn.Nheko.yaml | 2 +- resources/qml/Avatar.qml | 6 +- resources/qml/RoomSettings.qml | 4 +- resources/qml/ScrollHelper.qml | 7 +- resources/qml/components/AvatarListTile.qml | 133 ++++++++ .../qml/dialogs/ImagePackEditorDialog.qml | 283 ++++++++++++++++++ .../qml/dialogs/ImagePackSettingsDialog.qml | 174 +++-------- resources/res.qrc | 2 + src/Cache.cpp | 2 +- src/Cache_p.h | 34 ++- src/MxcImageProvider.cpp | 26 +- src/MxcImageProvider.h | 7 +- src/SingleImagePackModel.cpp | 181 +++++++++++ src/SingleImagePackModel.h | 38 ++- src/timeline/TimelineModel.cpp | 9 + src/timeline/TimelineModel.h | 8 +- 17 files changed, 751 insertions(+), 167 deletions(-) create mode 100644 resources/qml/components/AvatarListTile.qml create mode 100644 resources/qml/dialogs/ImagePackEditorDialog.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f824048..e8bc855d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb + GIT_TAG e5688a2c5987a614b5055595f991f18568127bd2 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index 0fa450b3..2c0c5ebf 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -161,7 +161,7 @@ modules: buildsystem: cmake-ninja name: mtxclient sources: - - commit: 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb + - commit: e5688a2c5987a614b5055595f991f18568127bd2 type: git url: https://github.com/Nheko-Reborn/mtxclient.git - config-opts: diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 6c12952a..9685dde1 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -11,10 +11,11 @@ import im.nheko 1.0 Rectangle { id: avatar - property alias url: img.source + property string url property string userid property string displayName property alias textColor: label.color + property bool crop: true signal clicked(var mouse) @@ -44,12 +45,13 @@ Rectangle { anchors.fill: parent asynchronous: true - fillMode: Image.PreserveAspectCrop + fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit mipmap: true smooth: true sourceSize.width: avatar.width sourceSize.height: avatar.height layer.enabled: true + source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale") MouseArea { id: mouseArea diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index 6ba080c4..69cf427c 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -154,7 +154,7 @@ ApplicationWindow { GridLayout { columns: 2 - rowSpacing: 10 + rowSpacing: Nheko.paddingLarge MatrixText { text: qsTr("SETTINGS") @@ -180,7 +180,7 @@ ApplicationWindow { } MatrixText { - text: "Room access" + text: qsTr("Room access") Layout.fillWidth: true } diff --git a/resources/qml/ScrollHelper.qml b/resources/qml/ScrollHelper.qml index 2dd56f27..e584ae3d 100644 --- a/resources/qml/ScrollHelper.qml +++ b/resources/qml/ScrollHelper.qml @@ -30,6 +30,10 @@ MouseArea { property alias enabled: root.enabled function calculateNewPosition(flickableItem, wheel) { + // breaks ListView's with headers... + //if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) + // minYExtent += flickableItem.headerItem.height; + //Nothing to scroll if (flickableItem.contentHeight < flickableItem.height) return flickableItem.contentY; @@ -55,9 +59,6 @@ MouseArea { var minYExtent = flickableItem.originY + flickableItem.topMargin; var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; - if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) - minYExtent += flickableItem.headerItem.height; - //Avoid overscrolling return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); } diff --git a/resources/qml/components/AvatarListTile.qml b/resources/qml/components/AvatarListTile.qml new file mode 100644 index 00000000..36c26a97 --- /dev/null +++ b/resources/qml/components/AvatarListTile.qml @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import im.nheko 1.0 + +Rectangle { + id: tile + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) + required property string avatarUrl + required property string title + required property string subtitle + required property int index + required property int selectedIndex + property bool crop: true + + color: background + height: avatarSize + 2 * Nheko.paddingMedium + width: ListView.view.width + state: "normal" + states: [ + State { + name: "highlight" + when: hovered.hovered && !(index == selectedIndex) + + PropertyChanges { + target: tile + background: Nheko.colors.dark + importantText: Nheko.colors.brightText + unimportantText: Nheko.colors.brightText + bubbleBackground: Nheko.colors.highlight + bubbleText: Nheko.colors.highlightedText + } + + }, + State { + name: "selected" + when: index == selectedIndex + + PropertyChanges { + target: tile + background: Nheko.colors.highlight + importantText: Nheko.colors.highlightedText + unimportantText: Nheko.colors.highlightedText + bubbleBackground: Nheko.colors.highlightedText + bubbleText: Nheko.colors.highlight + } + + } + ] + + HoverHandler { + id: hovered + } + + RowLayout { + spacing: Nheko.paddingMedium + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + + Avatar { + id: avatar + + enabled: false + Layout.alignment: Qt.AlignVCenter + height: avatarSize + width: avatarSize + url: tile.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: title + crop: tile.crop + } + + ColumnLayout { + id: textContent + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + Layout.minimumWidth: 100 + width: parent.width - avatar.width + Layout.preferredWidth: parent.width - avatar.width + spacing: Nheko.paddingSmall + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + ElidedLabel { + Layout.alignment: Qt.AlignBottom + color: tile.importantText + elideWidth: textContent.width - Nheko.paddingMedium + fullText: title + textFormat: Text.PlainText + } + + Item { + Layout.fillWidth: true + } + + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + ElidedLabel { + color: tile.unimportantText + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + elideWidth: textContent.width - Nheko.paddingSmall + fullText: subtitle + textFormat: Text.PlainText + } + + Item { + Layout.fillWidth: true + } + + } + + } + + } + +} diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml new file mode 100644 index 00000000..0049d3b4 --- /dev/null +++ b/resources/qml/dialogs/ImagePackEditorDialog.qml @@ -0,0 +1,283 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import "../components" +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import im.nheko 1.0 + +ApplicationWindow { + //Component.onCompleted: Nheko.reparent(win) + + id: win + + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) + property SingleImagePackModel imagePack + property int currentImageIndex: -1 + readonly property int stickerDim: 128 + readonly property int stickerDimPad: 128 + Nheko.paddingSmall + + title: qsTr("Editing image pack") + height: 600 + width: 600 + palette: Nheko.colors + color: Nheko.colors.base + modality: Qt.WindowModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint + + AdaptiveLayout { + id: adaptiveView + + anchors.fill: parent + singlePageMode: false + pageIndex: 0 + + AdaptiveLayoutElement { + id: packlistC + + visible: Settings.groupView + minimumWidth: 200 + collapsedWidth: 200 + preferredWidth: 300 + maximumWidth: 300 + clip: true + + ListView { + //required property bool isEmote + //required property bool isSticker + + model: imagePack + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + header: AvatarListTile { + title: imagePack.packname + avatarUrl: imagePack.avatarUrl + subtitle: imagePack.statekey + index: -1 + selectedIndex: currentImageIndex + + TapHandler { + onSingleTapped: currentImageIndex = -1 + } + + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + height: parent.height - Nheko.paddingSmall * 2 + width: 3 + color: Nheko.colors.highlight + } + + } + + delegate: AvatarListTile { + id: packItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + required property string shortCode + required property string url + required property string body + + title: shortCode + subtitle: body + avatarUrl: url + selectedIndex: currentImageIndex + crop: false + + TapHandler { + onSingleTapped: currentImageIndex = index + } + + } + + } + + } + + AdaptiveLayoutElement { + id: packinfoC + + Rectangle { + color: Nheko.colors.window + + GridLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + visible: currentImageIndex == -1 + enabled: visible + columns: 2 + rowSpacing: Nheko.paddingLarge + + Avatar { + Layout.columnSpan: 2 + url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: imagePack.packname + height: 130 + width: 130 + crop: false + Layout.alignment: Qt.AlignHCenter + } + + MatrixText { + visible: imagePack.roomid + text: qsTr("State key") + } + + MatrixTextField { + visible: imagePack.roomid + Layout.fillWidth: true + text: imagePack.statekey + onTextEdited: imagePack.statekey = text + } + + MatrixText { + text: qsTr("Packname") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.packname + onTextEdited: imagePack.packname = text + } + + MatrixText { + text: qsTr("Attrbution") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.attribution + onTextEdited: imagePack.attribution = text + } + + MatrixText { + text: qsTr("Use as Emoji") + } + + ToggleButton { + checked: imagePack.isEmotePack + onToggled: imagePack.isEmotePack = checked + Layout.alignment: Qt.AlignRight + } + + MatrixText { + text: qsTr("Use as Sticker") + } + + ToggleButton { + checked: imagePack.isStickerPack + onToggled: imagePack.isStickerPack = checked + Layout.alignment: Qt.AlignRight + } + + Item { + Layout.columnSpan: 2 + Layout.fillHeight: true + } + + } + + GridLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + visible: currentImageIndex >= 0 + enabled: visible + columns: 2 + rowSpacing: Nheko.paddingLarge + + Avatar { + Layout.columnSpan: 2 + url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/") + displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode) + height: 130 + width: 130 + crop: false + Layout.alignment: Qt.AlignHCenter + } + + MatrixText { + text: qsTr("Shortcode") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode) + onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.ShortCode) + } + + MatrixText { + text: qsTr("Body") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Body) + onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.Body) + } + + MatrixText { + text: qsTr("Use as Emoji") + } + + ToggleButton { + checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsEmote) + onToggled: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.IsEmote) + Layout.alignment: Qt.AlignRight + } + + MatrixText { + text: qsTr("Use as Sticker") + } + + ToggleButton { + checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsSticker) + onToggled: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.IsSticker) + Layout.alignment: Qt.AlignRight + } + + Item { + Layout.columnSpan: 2 + Layout.fillHeight: true + } + + } + + } + + } + + } + + footer: DialogButtonBox { + id: buttons + + Button { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole + onClicked: win.close() + } + + Button { + text: qsTr("Save") + DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole + onClicked: { + imagePack.save(); + win.close(); + } + } + + } + +} diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml index 3d830bf7..c57867fd 100644 --- a/resources/qml/dialogs/ImagePackSettingsDialog.qml +++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml @@ -20,14 +20,22 @@ ApplicationWindow { readonly property int stickerDimPad: 128 + Nheko.paddingSmall title: qsTr("Image pack settings") - height: 400 - width: 600 + height: 600 + width: 800 palette: Nheko.colors color: Nheko.colors.base modality: Qt.NonModal flags: Qt.Dialog | Qt.WindowCloseButtonHint Component.onCompleted: Nheko.reparent(win) + Component { + id: packEditor + + ImagePackEditorDialog { + } + + } + AdaptiveLayout { id: adaptiveView @@ -54,7 +62,7 @@ ApplicationWindow { enabled: !Settings.mobileMode } - delegate: Rectangle { + delegate: AvatarListTile { id: packItem property color background: Nheko.colors.window @@ -63,131 +71,24 @@ ApplicationWindow { property color bubbleBackground: Nheko.colors.highlight property color bubbleText: Nheko.colors.highlightedText required property string displayName - required property string avatarUrl required property bool fromAccountData required property bool fromCurrentRoom - required property int index - color: background - height: avatarSize + 2 * Nheko.paddingMedium - width: ListView.view.width - state: "normal" - states: [ - State { - name: "highlight" - when: hovered.hovered && !(index == currentPackIndex) - - PropertyChanges { - target: packItem - background: Nheko.colors.dark - importantText: Nheko.colors.brightText - unimportantText: Nheko.colors.brightText - bubbleBackground: Nheko.colors.highlight - bubbleText: Nheko.colors.highlightedText - } - - }, - State { - name: "selected" - when: index == currentPackIndex - - PropertyChanges { - target: packItem - background: Nheko.colors.highlight - importantText: Nheko.colors.highlightedText - unimportantText: Nheko.colors.highlightedText - bubbleBackground: Nheko.colors.highlightedText - bubbleText: Nheko.colors.highlight - } - - } - ] + title: displayName + subtitle: { + if (fromAccountData) + return qsTr("Private pack"); + else if (fromCurrentRoom) + return qsTr("Pack from this room"); + else + return qsTr("Globally enabled pack"); + } + selectedIndex: currentPackIndex TapHandler { - margin: -Nheko.paddingSmall onSingleTapped: currentPackIndex = index } - HoverHandler { - id: hovered - } - - RowLayout { - spacing: Nheko.paddingMedium - anchors.fill: parent - anchors.margins: Nheko.paddingMedium - - Avatar { - // In the future we could show an online indicator by setting the userid for the avatar - //userid: Nheko.currentUser.userid - - id: avatar - - enabled: false - Layout.alignment: Qt.AlignVCenter - height: avatarSize - width: avatarSize - url: avatarUrl.replace("mxc://", "image://MxcImage/") - displayName: packItem.displayName - } - - ColumnLayout { - id: textContent - - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - Layout.minimumWidth: 100 - width: parent.width - avatar.width - Layout.preferredWidth: parent.width - avatar.width - spacing: Nheko.paddingSmall - - RowLayout { - Layout.fillWidth: true - spacing: 0 - - ElidedLabel { - Layout.alignment: Qt.AlignBottom - color: packItem.importantText - elideWidth: textContent.width - Nheko.paddingMedium - fullText: displayName - textFormat: Text.PlainText - } - - Item { - Layout.fillWidth: true - } - - } - - RowLayout { - Layout.fillWidth: true - spacing: 0 - - ElidedLabel { - color: packItem.unimportantText - font.pixelSize: fontMetrics.font.pixelSize * 0.9 - elideWidth: textContent.width - Nheko.paddingSmall - fullText: { - if (fromAccountData) - return qsTr("Private pack"); - else if (fromCurrentRoom) - return qsTr("Pack from this room"); - else - return qsTr("Globally enabled pack"); - } - textFormat: Text.PlainText - } - - Item { - Layout.fillWidth: true - } - - } - - } - - } - } } @@ -201,15 +102,10 @@ ApplicationWindow { color: Nheko.colors.window ColumnLayout { - //Button { - // Layout.alignment: Qt.AlignHCenter - // text: qsTr("Edit") - // enabled: currentPack.canEdit - //} - id: packinfo property string packName: currentPack ? currentPack.packname : "" + property string attribution: currentPack ? currentPack.attribution : "" property string avatarUrl: currentPack ? currentPack.avatarUrl : "" anchors.fill: parent @@ -227,8 +123,18 @@ ApplicationWindow { MatrixText { text: packinfo.packName - font.pixelSize: 24 + font.pixelSize: Math.ceil(fontMetrics.pixelSize * 1.1) + horizontalAlignment: TextEdit.AlignHCenter Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2 + } + + MatrixText { + text: packinfo.attribution + wrapMode: TextEdit.Wrap + horizontalAlignment: TextEdit.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2 } GridLayout { @@ -250,6 +156,18 @@ ApplicationWindow { } + Button { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Edit") + enabled: currentPack.canEdit + onClicked: { + var dialog = packEditor.createObject(timelineRoot, { + "imagePack": currentPack + }); + dialog.show(); + } + } + GridView { Layout.fillHeight: true Layout.fillWidth: true @@ -272,7 +190,7 @@ ApplicationWindow { width: stickerDim height: stickerDim hoverEnabled: true - ToolTip.text: ":" + model.shortcode + ": - " + model.body + ToolTip.text: ":" + model.shortCode + ": - " + model.body ToolTip.visible: hovered contentItem: Image { diff --git a/resources/res.qrc b/resources/res.qrc index c911653c..d7187f42 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -160,6 +160,7 @@ qml/device-verification/Success.qml qml/dialogs/InputDialog.qml qml/dialogs/ImagePackSettingsDialog.qml + qml/dialogs/ImagePackEditorDialog.qml qml/ui/Ripple.qml qml/ui/Spinner.qml qml/ui/animations/BlinkAnimation.qml @@ -173,6 +174,7 @@ qml/voip/VideoCall.qml qml/components/AdaptiveLayout.qml qml/components/AdaptiveLayoutElement.qml + qml/components/AvatarListTile.qml qml/components/FlatButton.qml qml/RoomMembers.qml qml/InviteDialog.qml diff --git a/src/Cache.cpp b/src/Cache.cpp index 291df053..f3f3dbb6 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -125,7 +125,7 @@ template bool containsStateUpdates(const T &e) { - return std::visit([](const auto &ev) { return Cache::isStateEvent(ev); }, e); + return std::visit([](const auto &ev) { return Cache::isStateEvent_; }, e); } bool diff --git a/src/Cache_p.h b/src/Cache_p.h index 5d700658..30c365a6 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -291,15 +291,9 @@ public: std::optional secret(const std::string name); template - static constexpr bool isStateEvent(const mtx::events::StateEvent &) - { - return true; - } - template - static constexpr bool isStateEvent(const mtx::events::Event &) - { - return false; - } + constexpr static bool isStateEvent_ = + std::is_same_v>, + mtx::events::StateEvent().content)>>; static int compare_state_key(const MDB_val *a, const MDB_val *b) { @@ -416,11 +410,27 @@ private: } std::visit( - [&txn, &statesdb, &stateskeydb, &eventsDb](auto e) { - if constexpr (isStateEvent(e)) { + [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) { + if constexpr (isStateEvent_) { eventsDb.put(txn, e.event_id, json(e).dump()); - if (e.type != EventType::Unsupported) { + if (std::is_same_v< + std::remove_cv_t>, + StateEvent>) { + if (e.type == EventType::RoomMember) + membersdb.del(txn, e.state_key, ""); + else if (e.state_key.empty()) + statesdb.del(txn, to_string(e.type)); + else + stateskeydb.del( + txn, + to_string(e.type), + json::object({ + {"key", e.state_key}, + {"id", e.event_id}, + }) + .dump()); + } else if (e.type != EventType::Unsupported) { if (e.state_key.empty()) statesdb.put( txn, to_string(e.type), json(e).dump()); diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index ab0f8152..b8648269 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -22,7 +22,14 @@ QHash infos; QQuickImageResponse * MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) { - MxcImageResponse *response = new MxcImageResponse(id, requestedSize); + auto id_ = id; + bool crop = true; + if (id.endsWith("?scale")) { + crop = false; + id_.remove("?scale"); + } + + MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize); pool.start(response); return response; } @@ -36,20 +43,24 @@ void MxcImageResponse::run() { MxcImageProvider::download( - m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) { + m_id, + m_requestedSize, + [this](QString, QSize, QImage image, QString) { if (image.isNull()) { m_error = "Failed to download image."; } else { m_image = image; } emit finished(); - }); + }, + m_crop); } void MxcImageProvider::download(const QString &id, const QSize &requestedSize, - std::function then) + std::function then, + bool crop) { std::optional encryptionInfo; auto temp = infos.find("mxc://" + id); @@ -58,11 +69,12 @@ MxcImageProvider::download(const QString &id, if (requestedSize.isValid() && !encryptionInfo) { QString fileName = - QString("%1_%2x%3_crop") + QString("%1_%2x%3_%4") .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))) .arg(requestedSize.width()) - .arg(requestedSize.height()); + .arg(requestedSize.height()) + .arg(crop ? "crop" : "scale"); QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/media_cache", fileName); @@ -85,7 +97,7 @@ MxcImageProvider::download(const QString &id, opts.mxc_url = "mxc://" + id.toStdString(); opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1; opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1; - opts.method = "crop"; + opts.method = crop ? "crop" : "scale"; http::client()->get_thumbnail( opts, [fileInfo, requestedSize, then, id](const std::string &res, diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index 7b960836..61d82852 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -19,9 +19,10 @@ class MxcImageResponse , public QRunnable { public: - MxcImageResponse(const QString &id, const QSize &requestedSize) + MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize) : m_id(id) , m_requestedSize(requestedSize) + , m_crop(crop) { setAutoDelete(false); } @@ -37,6 +38,7 @@ public: QString m_id, m_error; QSize m_requestedSize; QImage m_image; + bool m_crop; }; class MxcImageProvider @@ -51,7 +53,8 @@ public slots: static void addEncryptionInfo(mtx::crypto::EncryptedFile info); static void download(const QString &id, const QSize &requestedSize, - std::function then); + std::function then, + bool crop = true); private: QThreadPool pool; diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp index 6c508da0..d3cc8014 100644 --- a/src/SingleImagePackModel.cpp +++ b/src/SingleImagePackModel.cpp @@ -5,12 +5,18 @@ #include "SingleImagePackModel.h" #include "Cache_p.h" +#include "ChatPage.h" #include "MatrixClient.h" +#include "timeline/Permissions.h" +#include "timeline/TimelineModel.h" + +#include "Logging.h" SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) : QAbstractListModel(parent) , roomid_(std::move(pack_.source_room)) , statekey_(std::move(pack_.state_key)) + , old_statekey_(statekey_) , pack(std::move(pack_.pack)) { if (!pack.pack) @@ -61,6 +67,73 @@ SingleImagePackModel::data(const QModelIndex &index, int role) const return {}; } +bool +SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + using mtx::events::msc2545::PackUsage; + + if (hasIndex(index.row(), index.column(), index.parent())) { + auto &img = pack.images.at(shortcodes.at(index.row())); + switch (role) { + case ShortCode: { + auto newCode = value.toString().toStdString(); + + // otherwise we delete this by accident + if (pack.images.count(newCode)) + return false; + + auto tmp = img; + auto oldCode = shortcodes.at(index.row()); + pack.images.erase(oldCode); + shortcodes[index.row()] = newCode; + pack.images.insert({newCode, tmp}); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::ShortCode}); + return true; + } + case Body: + img.body = value.toString().toStdString(); + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::Body}); + return true; + case IsEmote: { + bool isEmote = value.toBool(); + bool isSticker = + img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); + + img.usage.set(PackUsage::Emoji, isEmote); + img.usage.set(PackUsage::Sticker, isSticker); + + if (img.usage == pack.pack->usage) + img.usage.reset(); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::IsEmote}); + + return true; + } + case IsSticker: { + bool isEmote = + img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); + bool isSticker = value.toBool(); + + img.usage.set(PackUsage::Emoji, isEmote); + img.usage.set(PackUsage::Sticker, isSticker); + + if (img.usage == pack.pack->usage) + img.usage.reset(); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::IsSticker}); + + return true; + } + } + } + return false; +} + bool SingleImagePackModel::isGloballyEnabled() const { @@ -98,3 +171,111 @@ SingleImagePackModel::setGloballyEnabled(bool enabled) // emit this->globallyEnabledChanged(); }); } + +bool +SingleImagePackModel::canEdit() const +{ + if (roomid_.empty()) + return true; + else + return Permissions(QString::fromStdString(roomid_)) + .canChange(qml_mtx_events::ImagePackInRoom); +} + +void +SingleImagePackModel::setPackname(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->display_name) { + this->pack.pack->display_name = val_; + emit packnameChanged(); + } +} + +void +SingleImagePackModel::setAttribution(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->attribution) { + this->pack.pack->attribution = val_; + emit attributionChanged(); + } +} + +void +SingleImagePackModel::setAvatarUrl(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->avatar_url) { + this->pack.pack->avatar_url = val_; + emit avatarUrlChanged(); + } +} + +void +SingleImagePackModel::setStatekey(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != statekey_) { + statekey_ = val_; + emit statekeyChanged(); + } +} + +void +SingleImagePackModel::setIsStickerPack(bool val) +{ + using mtx::events::msc2545::PackUsage; + if (val != pack.pack->is_sticker()) { + pack.pack->usage.set(PackUsage::Sticker, val); + emit isStickerPackChanged(); + } +} + +void +SingleImagePackModel::setIsEmotePack(bool val) +{ + using mtx::events::msc2545::PackUsage; + if (val != pack.pack->is_emoji()) { + pack.pack->usage.set(PackUsage::Emoji, val); + emit isEmotePackChanged(); + } +} + +void +SingleImagePackModel::save() +{ + if (roomid_.empty()) { + http::client()->put_account_data(pack, [this](mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to update image pack: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + }); + } else { + if (old_statekey_ != statekey_) { + http::client()->send_state_event( + roomid_, + to_string(mtx::events::EventType::ImagePackInRoom), + old_statekey_, + nlohmann::json::object(), + [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to delete old image pack: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + }); + } + + http::client()->send_state_event( + roomid_, + statekey_, + pack, + [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to update image pack: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + }); + } +} diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h index e0c791ba..44f413c6 100644 --- a/src/SingleImagePackModel.h +++ b/src/SingleImagePackModel.h @@ -15,14 +15,18 @@ class SingleImagePackModel : public QAbstractListModel Q_OBJECT Q_PROPERTY(QString roomid READ roomid CONSTANT) - Q_PROPERTY(QString statekey READ statekey CONSTANT) - Q_PROPERTY(QString attribution READ statekey CONSTANT) - Q_PROPERTY(QString packname READ packname CONSTANT) - Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT) - Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT) - Q_PROPERTY(bool isEmotePack READ isEmotePack CONSTANT) + Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged) + Q_PROPERTY( + QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged) + Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY( + bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged) + Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged) Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY globallyEnabledChanged) + Q_PROPERTY(bool canEdit READ canEdit CONSTANT) + public: enum Roles { @@ -32,11 +36,15 @@ public: IsEmote, IsSticker, }; + Q_ENUM(Roles); SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr); QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, + const QVariant &value, + int role = Qt::EditRole) override; QString roomid() const { return QString::fromStdString(roomid_); } QString statekey() const { return QString::fromStdString(statekey_); } @@ -47,14 +55,30 @@ public: bool isEmotePack() const { return pack.pack->is_emoji(); } bool isGloballyEnabled() const; + bool canEdit() const; void setGloballyEnabled(bool enabled); + void setPackname(QString val); + void setAttribution(QString val); + void setAvatarUrl(QString val); + void setStatekey(QString val); + void setIsStickerPack(bool val); + void setIsEmotePack(bool val); + + Q_INVOKABLE void save(); + signals: void globallyEnabledChanged(); + void statekeyChanged(); + void attributionChanged(); + void packnameChanged(); + void avatarUrlChanged(); + void isEmotePackChanged(); + void isStickerPackChanged(); private: std::string roomid_; - std::string statekey_; + std::string statekey_, old_statekey_; mtx::events::msc2545::ImagePack pack; std::vector shortcodes; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index a8adf05b..10d9788d 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -308,6 +308,15 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t) case qml_mtx_events::KeyVerificationDone: case qml_mtx_events::KeyVerificationReady: return mtx::events::EventType::RoomMessage; + //! m.image_pack, currently im.ponies.room_emotes + case qml_mtx_events::ImagePackInRoom: + return mtx::events::EventType::ImagePackRooms; + //! m.image_pack, currently im.ponies.user_emotes + case qml_mtx_events::ImagePackInAccountData: + return mtx::events::EventType::ImagePackInAccountData; + //! m.image_pack.rooms, currently im.ponies.emote_rooms + case qml_mtx_events::ImagePackRooms: + return mtx::events::EventType::ImagePackRooms; default: return mtx::events::EventType::Unsupported; }; diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index f62c5360..b5c8ca37 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -107,7 +107,13 @@ enum EventType KeyVerificationCancel, KeyVerificationKey, KeyVerificationDone, - KeyVerificationReady + KeyVerificationReady, + //! m.image_pack, currently im.ponies.room_emotes + ImagePackInRoom, + //! m.image_pack, currently im.ponies.user_emotes + ImagePackInAccountData, + //! m.image_pack.rooms, currently im.ponies.emote_rooms + ImagePackRooms, }; Q_ENUM_NS(EventType) mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType); From 16d0190f4e1ee79025ec47f3dcfa1fb701a63ff1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 6 Aug 2021 03:28:56 +0200 Subject: [PATCH 38/45] Allow uploading additional stickers --- resources/qml/ScrollHelper.qml | 7 +- .../qml/dialogs/ImagePackEditorDialog.qml | 18 +++++ src/SingleImagePackModel.cpp | 69 ++++++++++++++++++- src/SingleImagePackModel.h | 8 +++ 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/resources/qml/ScrollHelper.qml b/resources/qml/ScrollHelper.qml index e584ae3d..e84e67fd 100644 --- a/resources/qml/ScrollHelper.qml +++ b/resources/qml/ScrollHelper.qml @@ -23,6 +23,9 @@ MouseArea { // console.warn("Delta: ", wheel.pixelDelta.y); // console.warn("Old position: ", flickable.contentY); // console.warn("New position: ", newPos); + // breaks ListView's with headers... + //if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) + // minYExtent += flickableItem.headerItem.height; id: root @@ -30,10 +33,6 @@ MouseArea { property alias enabled: root.enabled function calculateNewPosition(flickableItem, wheel) { - // breaks ListView's with headers... - //if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) - // minYExtent += flickableItem.headerItem.height; - //Nothing to scroll if (flickableItem.contentHeight < flickableItem.height) return flickableItem.contentY; diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml index 0049d3b4..89301215 100644 --- a/resources/qml/dialogs/ImagePackEditorDialog.qml +++ b/resources/qml/dialogs/ImagePackEditorDialog.qml @@ -4,6 +4,7 @@ import ".." import "../components" +import Qt.labs.platform 1.1 import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 @@ -78,6 +79,23 @@ ApplicationWindow { } + footer: Button { + palette: Nheko.colors + onClicked: addFilesDialog.open() + width: ListView.view.width + text: qsTr("Add images") + + FileDialog { + id: addFilesDialog + + folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation) + fileMode: FileDialog.OpenFiles + nameFilters: [qsTr("Stickers (*.png *.webp)")] + onAccepted: imagePack.addStickers(files) + } + + } + delegate: AvatarListTile { id: packItem diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp index d3cc8014..ddecf1ad 100644 --- a/src/SingleImagePackModel.cpp +++ b/src/SingleImagePackModel.cpp @@ -4,13 +4,18 @@ #include "SingleImagePackModel.h" +#include +#include + #include "Cache_p.h" #include "ChatPage.h" +#include "Logging.h" #include "MatrixClient.h" +#include "Utils.h" #include "timeline/Permissions.h" #include "timeline/TimelineModel.h" -#include "Logging.h" +Q_DECLARE_METATYPE(mtx::common::ImageInfo); SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) : QAbstractListModel(parent) @@ -19,11 +24,15 @@ SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) , old_statekey_(statekey_) , pack(std::move(pack_.pack)) { + [[maybe_unused]] static auto imageInfoType = qRegisterMetaType(); + if (!pack.pack) pack.pack = mtx::events::msc2545::ImagePack::PackDescription{}; for (const auto &e : pack.images) shortcodes.push_back(e.first); + + connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb); } int @@ -279,3 +288,61 @@ SingleImagePackModel::save() }); } } + +void +SingleImagePackModel::addStickers(QList files) +{ + for (const auto &f : files) { + auto file = QFile(f.toLocalFile()); + if (!file.open(QFile::ReadOnly)) { + ChatPage::instance()->showNotification( + tr("Failed to open image: {}").arg(f.toLocalFile())); + return; + } + + auto bytes = file.readAll(); + auto img = utils::readImage(bytes); + + mtx::common::ImageInfo info{}; + + auto sz = img.size() / 2; + if (sz.width() > 512 || sz.height() > 512) { + sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio); + } + + info.h = sz.height(); + info.w = sz.width(); + info.size = bytes.size(); + + auto filename = f.fileName().toStdString(); + http::client()->upload( + bytes.toStdString(), + QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(), + filename, + [this, filename, info](const mtx::responses::ContentURI &uri, + mtx::http::RequestErr e) { + if (e) { + ChatPage::instance()->showNotification( + tr("Failed to upload image: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + return; + } + + emit addImage(uri.content_uri, filename, info); + }); + } +} +void +SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info) +{ + mtx::events::msc2545::PackImage img{}; + img.url = uri; + img.info = info; + beginInsertRows( + QModelIndex(), static_cast(shortcodes.size()), static_cast(shortcodes.size())); + + pack.images[filename] = img; + shortcodes.push_back(filename); + + endInsertRows(); +} diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h index 44f413c6..cd38b3b6 100644 --- a/src/SingleImagePackModel.h +++ b/src/SingleImagePackModel.h @@ -5,6 +5,8 @@ #pragma once #include +#include +#include #include @@ -66,6 +68,7 @@ public: void setIsEmotePack(bool val); Q_INVOKABLE void save(); + Q_INVOKABLE void addStickers(QList files); signals: void globallyEnabledChanged(); @@ -76,6 +79,11 @@ signals: void isEmotePackChanged(); void isStickerPackChanged(); + void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info); + +private slots: + void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info); + private: std::string roomid_; std::string statekey_, old_statekey_; From e5a6b2b6efcb56072146aad5996132070f9c2078 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 6 Aug 2021 04:31:30 +0200 Subject: [PATCH 39/45] Allow creating new packs --- .../qml/dialogs/ImagePackEditorDialog.qml | 8 +++--- .../qml/dialogs/ImagePackSettingsDialog.qml | 28 +++++++++++++++++++ src/Cache.cpp | 2 +- src/ImagePackListModel.cpp | 18 ++++++++++++ src/ImagePackListModel.h | 4 +++ src/SingleImagePackModel.cpp | 4 ++- 6 files changed, 58 insertions(+), 6 deletions(-) diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml index 89301215..b839c9e3 100644 --- a/resources/qml/dialogs/ImagePackEditorDialog.qml +++ b/resources/qml/dialogs/ImagePackEditorDialog.qml @@ -186,7 +186,7 @@ ApplicationWindow { ToggleButton { checked: imagePack.isEmotePack - onToggled: imagePack.isEmotePack = checked + onClicked: imagePack.isEmotePack = checked Layout.alignment: Qt.AlignRight } @@ -196,7 +196,7 @@ ApplicationWindow { ToggleButton { checked: imagePack.isStickerPack - onToggled: imagePack.isStickerPack = checked + onClicked: imagePack.isStickerPack = checked Layout.alignment: Qt.AlignRight } @@ -251,7 +251,7 @@ ApplicationWindow { ToggleButton { checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsEmote) - onToggled: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.IsEmote) + onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsEmote) Layout.alignment: Qt.AlignRight } @@ -261,7 +261,7 @@ ApplicationWindow { ToggleButton { checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsSticker) - onToggled: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.IsSticker) + onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsSticker) Layout.alignment: Qt.AlignRight } diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml index c57867fd..5181619c 100644 --- a/resources/qml/dialogs/ImagePackSettingsDialog.qml +++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml @@ -62,6 +62,34 @@ ApplicationWindow { enabled: !Settings.mobileMode } + footer: ColumnLayout { + Button { + palette: Nheko.colors + onClicked: { + var dialog = packEditor.createObject(timelineRoot, { + "imagePack": packlist.newPack(false) + }); + dialog.show(); + } + width: packlist.width + visible: !packlist.containsAccountPack + text: qsTr("Create account pack") + } + + Button { + palette: Nheko.colors + onClicked: { + var dialog = packEditor.createObject(timelineRoot, { + "imagePack": packlist.newPack(true) + }); + dialog.show(); + } + width: packlist.width + text: qsTr("New room pack") + } + + } + delegate: AvatarListTile { id: packItem diff --git a/src/Cache.cpp b/src/Cache.cpp index f3f3dbb6..6650334a 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3401,7 +3401,7 @@ Cache::getImagePacks(const std::string &room_id, std::optional stickers) info.pack.pack = pack.pack; for (const auto &img : pack.images) { - if (img.second.overrides_usage() && + if (stickers.has_value() && img.second.overrides_usage() && (stickers ? !img.second.is_sticker() : !img.second.is_emoji())) continue; diff --git a/src/ImagePackListModel.cpp b/src/ImagePackListModel.cpp index 89f1f68e..6392de22 100644 --- a/src/ImagePackListModel.cpp +++ b/src/ImagePackListModel.cpp @@ -74,3 +74,21 @@ ImagePackListModel::packAt(int row) QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership); return e; } + +SingleImagePackModel * +ImagePackListModel::newPack(bool inRoom) +{ + ImagePackInfo info{}; + if (inRoom) + info.source_room = room_id; + return new SingleImagePackModel(info); +} + +bool +ImagePackListModel::containsAccountPack() const +{ + for (const auto &p : packs) + if (p->roomid().isEmpty()) + return true; + return false; +} diff --git a/src/ImagePackListModel.h b/src/ImagePackListModel.h index 0a044690..2aa5abb2 100644 --- a/src/ImagePackListModel.h +++ b/src/ImagePackListModel.h @@ -12,6 +12,7 @@ class SingleImagePackModel; class ImagePackListModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT) public: enum Roles { @@ -29,6 +30,9 @@ public: QVariant data(const QModelIndex &index, int role) const override; Q_INVOKABLE SingleImagePackModel *packAt(int row); + Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom); + + bool containsAccountPack() const; private: std::string room_id; diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp index ddecf1ad..dea25264 100644 --- a/src/SingleImagePackModel.cpp +++ b/src/SingleImagePackModel.cpp @@ -15,7 +15,7 @@ #include "timeline/Permissions.h" #include "timeline/TimelineModel.h" -Q_DECLARE_METATYPE(mtx::common::ImageInfo); +Q_DECLARE_METATYPE(mtx::common::ImageInfo) SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) : QAbstractListModel(parent) @@ -285,6 +285,8 @@ SingleImagePackModel::save() ChatPage::instance()->showNotification( tr("Failed to update image pack: {}") .arg(QString::fromStdString(e->matrix_error.error))); + + nhlog::net()->info("Uploaded image pack: {}", statekey_); }); } } From cc22309c5b549d068e4cb1f4da91d346bb76562f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 6 Aug 2021 04:43:56 +0200 Subject: [PATCH 40/45] this is not needed for translations --- src/SingleImagePackModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp index dea25264..7bf55617 100644 --- a/src/SingleImagePackModel.cpp +++ b/src/SingleImagePackModel.cpp @@ -255,7 +255,7 @@ void SingleImagePackModel::save() { if (roomid_.empty()) { - http::client()->put_account_data(pack, [this](mtx::http::RequestErr e) { + http::client()->put_account_data(pack, [](mtx::http::RequestErr e) { if (e) ChatPage::instance()->showNotification( tr("Failed to update image pack: {}") @@ -268,7 +268,7 @@ SingleImagePackModel::save() to_string(mtx::events::EventType::ImagePackInRoom), old_statekey_, nlohmann::json::object(), - [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { + [](const mtx::responses::EventId &, mtx::http::RequestErr e) { if (e) ChatPage::instance()->showNotification( tr("Failed to delete old image pack: {}") From 001f87fe7726be5a3cdf9a1c145f361740d90eae Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Aug 2021 01:00:18 +0200 Subject: [PATCH 41/45] Fix redactions --- CMakeLists.txt | 2 +- io.github.NhekoReborn.Nheko.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e8bc855d..55b58da1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG e5688a2c5987a614b5055595f991f18568127bd2 + GIT_TAG bcf363cb5e6c423f40c96123e227bc8c5f6d6f80 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index 2c0c5ebf..a363aeff 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -161,7 +161,7 @@ modules: buildsystem: cmake-ninja name: mtxclient sources: - - commit: e5688a2c5987a614b5055595f991f18568127bd2 + - commit: bcf363cb5e6c423f40c96123e227bc8c5f6d6f80 type: git url: https://github.com/Nheko-Reborn/mtxclient.git - config-opts: From 7a7ba47c013c0a229e8a4ac2e11107d4e4069948 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Aug 2021 01:24:13 +0200 Subject: [PATCH 42/45] Share shm in flatpak fixes #562 Requires flatpak 1.11.1 --- io.github.NhekoReborn.Nheko.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index a363aeff..638f278a 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -8,6 +8,7 @@ rename-desktop-file: nheko.desktop rename-appdata-file: nheko.appdata.xml finish-args: - --device=dri + - --device=shm # needed for webcams, see #517 - --device=all - --share=ipc @@ -19,6 +20,8 @@ finish-args: - --talk-name=org.freedesktop.secrets - --talk-name=org.freedesktop.StatusNotifierItem - --talk-name=org.kde.* + # needed for SingleApplication to work + - --allow=per-app-dev-shm cleanup: - /include - /bin/mdb* From 3e53b8cc09f63b1533f505b398ec7dfe4e8b3a43 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Aug 2021 01:38:46 +0200 Subject: [PATCH 43/45] Install recent flatpak in CI --- .gitlab-ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e24c5c85..cea6be7b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -91,7 +91,9 @@ build-flatpak-amd64: #image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master' tags: [docker] before_script: - - apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0 + # need flatpak 1.11.1 at least + - apt-get update && apt-get install -y software-properties-common + - add-apt-repository ppa:alexlarsson/flatpak && apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0 - flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --noninteractive install --user flathub org.kde.Platform//5.15 - flatpak --noninteractive install --user flathub org.kde.Sdk//5.15 @@ -119,7 +121,9 @@ build-flatpak-arm64: #image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master' tags: [docker-arm64] before_script: - - apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0 + # need flatpak 1.11.1 at least + - apt-get update && apt-get install -y software-properties-common + - add-apt-repository ppa:alexlarsson/flatpak && apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0 - flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --noninteractive install --user flathub org.kde.Platform//5.15 - flatpak --noninteractive install --user flathub org.kde.Sdk//5.15 From 483769a2a1b0e158c0ef7f332486ec644332e20b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Aug 2021 01:48:40 +0200 Subject: [PATCH 44/45] device=shm not needed in flatpak --- io.github.NhekoReborn.Nheko.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index 638f278a..a0e57b09 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -8,7 +8,6 @@ rename-desktop-file: nheko.desktop rename-appdata-file: nheko.appdata.xml finish-args: - --device=dri - - --device=shm # needed for webcams, see #517 - --device=all - --share=ipc From 72bbad7485db6ac1803f81344c29b93d9fa70945 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Aug 2021 22:51:09 +0200 Subject: [PATCH 45/45] Show encryption errors in qml and add request keys button --- CMakeLists.txt | 13 +- resources/qml/MessageView.qml | 2 + resources/qml/TimelineRow.qml | 3 + resources/qml/delegates/Encrypted.qml | 48 ++++ resources/qml/delegates/MessageDelegate.qml | 11 + resources/qml/delegates/Reply.qml | 2 + resources/res.qrc | 9 +- src/Olm.cpp | 2 +- src/Olm.h | 9 +- src/timeline/EventStore.cpp | 256 +++++++++----------- src/timeline/EventStore.h | 9 +- src/timeline/TimelineModel.cpp | 16 ++ src/timeline/TimelineModel.h | 3 + src/timeline/TimelineViewManager.cpp | 2 + 14 files changed, 220 insertions(+), 165 deletions(-) create mode 100644 resources/qml/delegates/Encrypted.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 55b58da1..049ed8a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -541,32 +541,33 @@ qt5_wrap_cpp(MOC_HEADERS src/AvatarProvider.h src/BlurhashProvider.h - src/Cache_p.h src/CacheCryptoStructs.h + src/Cache_p.h src/CallDevices.h src/CallManager.h src/ChatPage.h src/Clipboard.h + src/CombinedImagePackModel.h src/CompletionProxyModel.h src/DeviceVerificationFlow.h + src/ImagePackListModel.h src/InviteesModel.h src/LoginPage.h src/MainWindow.h src/MemberList.h src/MxcImageProvider.h - src/ReadReceiptsModel.h + src/Olm.h src/RegisterPage.h + src/RoomsModel.h src/SSOHandler.h - src/CombinedImagePackModel.h src/SingleImagePackModel.h - src/ImagePackListModel.h src/TrayIcon.h src/UserSettingsPage.h src/UsersModel.h - src/RoomsModel.h src/WebRTCSession.h src/WelcomePage.h - ) + src/ReadReceiptsModel.h +) # # Bundle translations. diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index f3e15d84..79cbd700 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -349,6 +349,7 @@ ScrollView { required property string callType required property var reactions required property int trustlevel + required property int encryptionError required property var timestamp required property int status required property int index @@ -456,6 +457,7 @@ ScrollView { callType: wrapper.callType reactions: wrapper.reactions trustlevel: wrapper.trustlevel + encryptionError: wrapper.encryptionError timestamp: wrapper.timestamp status: wrapper.status relatedEventCacheBuster: wrapper.relatedEventCacheBuster diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 6345f44c..c612479a 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -38,6 +38,7 @@ Item { required property string callType required property var reactions required property int trustlevel + required property int encryptionError required property var timestamp required property int status required property int relatedEventCacheBuster @@ -110,6 +111,7 @@ Item { roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? "" + encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? "" relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0 } @@ -136,6 +138,7 @@ Item { roomTopic: r.roomTopic roomName: r.roomName callType: r.callType + encryptionError: r.encryptionError relatedEventCacheBuster: r.relatedEventCacheBuster isReply: false } diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml new file mode 100644 index 00000000..cd00a9d4 --- /dev/null +++ b/resources/qml/delegates/Encrypted.qml @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 +import im.nheko 1.0 + +ColumnLayout { + id: r + + required property int encryptionError + required property string eventId + + width: parent ? parent.width : undefined + + MatrixText { + text: { + switch (encryptionError) { + case Olm.MissingSession: + return qsTr("There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient."); + case Olm.MissingSessionIndex: + return qsTr("This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message."); + case Olm.DbError: + return qsTr("There was an internal error reading the decryption key from the database."); + case Olm.DecryptionFailed: + return qsTr("There was an error decrypting this message."); + case Olm.ParsingFailed: + return qsTr("The message couldn't be parsed."); + case Olm.ReplayAttack: + return qsTr("The encryption key was reused! Someone is possibly trying to insert false messages into this chat!"); + default: + return qsTr("Unknown decryption error"); + } + } + color: Nheko.colors.buttonText + width: r ? r.width : undefined + } + + Button { + palette: Nheko.colors + visible: encryptionError == Olm.MissingSession || encryptionError == Olm.MissingSessionIndex + text: qsTr("Request key") + onClicked: room.requestKeyForEvent(eventId) + } + +} diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index a98c2a8b..a8bdf183 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -29,6 +29,7 @@ Item { required property string roomTopic required property string roomName required property string callType + required property int encryptionError required property int relatedEventCacheBuster height: chooser.childrenRect.height @@ -189,6 +190,16 @@ Item { } + DelegateChoice { + roleValue: MtxEvent.Encrypted + + Encrypted { + encryptionError: d.encryptionError + eventId: d.eventId + } + + } + DelegateChoice { roleValue: MtxEvent.Name diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 3e02a940..8bbce10e 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -30,6 +30,7 @@ Item { property string roomTopic property string roomName property string callType + property int encryptionError property int relatedEventCacheBuster width: parent.width @@ -97,6 +98,7 @@ Item { roomName: r.roomName callType: r.callType relatedEventCacheBuster: r.relatedEventCacheBuster + encryptionError: r.encryptionError enabled: false width: parent.width isReply: true diff --git a/resources/res.qrc b/resources/res.qrc index d7187f42..f50265ca 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -143,14 +143,15 @@ qml/emoji/StickerPicker.qml qml/UserProfile.qml qml/delegates/MessageDelegate.qml - qml/delegates/TextMessage.qml - qml/delegates/NoticeMessage.qml - qml/delegates/ImageMessage.qml - qml/delegates/PlayableMediaMessage.qml + qml/delegates/Encrypted.qml qml/delegates/FileMessage.qml + qml/delegates/ImageMessage.qml + qml/delegates/NoticeMessage.qml qml/delegates/Pill.qml qml/delegates/Placeholder.qml + qml/delegates/PlayableMediaMessage.qml qml/delegates/Reply.qml + qml/delegates/TextMessage.qml qml/device-verification/Waiting.qml qml/device-verification/DeviceVerification.qml qml/device-verification/DigitVerification.qml diff --git a/src/Olm.cpp b/src/Olm.cpp index 048a6c0f..293b12de 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -1069,7 +1069,7 @@ decryptEvent(const MegolmSessionIndex &index, mtx::events::collections::TimelineEvent te; mtx::events::collections::from_json(body, te); - return {std::nullopt, std::nullopt, std::move(te.data)}; + return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)}; } catch (std::exception &e) { return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt}; } diff --git a/src/Olm.h b/src/Olm.h index a18cbbfb..ac1a1617 100644 --- a/src/Olm.h +++ b/src/Olm.h @@ -14,9 +14,11 @@ constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2"; namespace olm { +Q_NAMESPACE -enum class DecryptionErrorCode +enum DecryptionErrorCode { + NoError, MissingSession, // Session was not found, retrieve from backup or request from other devices // and try again MissingSessionIndex, // Session was found, but it does not reach back enough to this index, @@ -25,14 +27,13 @@ enum class DecryptionErrorCode DecryptionFailed, // libolm error ParsingFailed, // Failed to parse the actual event ReplayAttack, // Megolm index reused - UnknownFingerprint, // Unknown device Fingerprint }; +Q_ENUM_NS(DecryptionErrorCode) struct DecryptionResult { - std::optional error; + DecryptionErrorCode error; std::optional error_message; - std::optional event; }; diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 9a91ff79..742f8dbb 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -20,8 +20,7 @@ Q_DECLARE_METATYPE(Reaction) -QCache EventStore::decryptedEvents_{ - 1000}; +QCache EventStore::decryptedEvents_{1000}; QCache EventStore::events_by_id_{ 1000}; QCache EventStore::events_{1000}; @@ -144,12 +143,16 @@ EventStore::EventStore(std::string room_id, QObject *) mtx::events::msg::Encrypted>) { auto event = decryptEvent({room_id_, e.event_id}, e); - if (auto dec = - std::get_if>(event)) { - emit updateFlowEventId( - event_id.event_id.to_string()); + if (event->event) { + if (auto dec = std::get_if< + mtx::events::RoomEvent< + mtx::events::msg:: + KeyVerificationRequest>>( + &event->event.value())) { + emit updateFlowEventId( + event_id.event_id + .to_string()); + } } } }); @@ -393,12 +396,12 @@ EventStore::handleSync(const mtx::responses::Timeline &events) if (auto encrypted = std::get_if>( &event)) { - mtx::events::collections::TimelineEvents *d_event = - decryptEvent({room_id_, encrypted->event_id}, *encrypted); - if (std::visit( + auto d_event = decryptEvent({room_id_, encrypted->event_id}, *encrypted); + if (d_event->event && + std::visit( [](auto e) { return (e.sender != utils::localUser().toStdString()); }, - *d_event)) { - handle_room_verification(*d_event); + *d_event->event)) { + handle_room_verification(*d_event->event); } } } @@ -599,11 +602,15 @@ EventStore::get(int idx, bool decrypt) events_.insert(index, event_ptr); } - if (decrypt) + if (decrypt) { if (auto encrypted = std::get_if>( - event_ptr)) - return decryptEvent({room_id_, encrypted->event_id}, *encrypted); + event_ptr)) { + auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted); + if (decrypted->event) + return &*decrypted->event; + } + } return event_ptr; } @@ -629,7 +636,7 @@ EventStore::indexToId(int idx) const return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx)); } -mtx::events::collections::TimelineEvents * +olm::DecryptionResult * EventStore::decryptEvent(const IdIndex &idx, const mtx::events::EncryptedEvent &e) { @@ -641,57 +648,24 @@ EventStore::decryptEvent(const IdIndex &idx, index.session_id = e.content.session_id; index.sender_key = e.content.sender_key; - auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) { - auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event)); + auto asCacheEntry = [&idx](olm::DecryptionResult &&event) { + auto event_ptr = new olm::DecryptionResult(std::move(event)); decryptedEvents_.insert(idx, event_ptr); return event_ptr; }; auto decryptionResult = olm::decryptEvent(index, e); - mtx::events::RoomEvent dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - if (decryptionResult.error) { - switch (*decryptionResult.error) { + switch (decryptionResult.error) { case olm::DecryptionErrorCode::MissingSession: case olm::DecryptionErrorCode::MissingSessionIndex: { - if (decryptionResult.error == olm::DecryptionErrorCode::MissingSession) - dummy.content.body = - tr("-- Encrypted Event (No keys found for decryption) --", - "Placeholder, when the message was not decrypted yet or can't " - "be " - "decrypted.") - .toStdString(); - else - dummy.content.body = - tr("-- Encrypted Event (Key not valid for this index) --", - "Placeholder, when the message can't be decrypted with this " - "key since it is not valid for this index ") - .toStdString(); nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", index.room_id, index.session_id, e.sender); - // we may not want to request keys during initial sync and such - if (suppressKeyRequests) - break; - // TODO: Check if this actually works and look in key backup - auto copy = e; - copy.room_id = room_id_; - if (pending_key_requests.count(e.content.session_id)) { - pending_key_requests.at(e.content.session_id) - .events.push_back(copy); - } else { - PendingKeyRequests request; - request.request_id = - "key_request." + http::client()->generate_txn_id(); - request.events.push_back(copy); - olm::send_key_request_for(copy, request.request_id); - pending_key_requests[e.content.session_id] = request; - } + + requestSession(e, false); break; } case olm::DecryptionErrorCode::DbError: @@ -701,12 +675,6 @@ EventStore::decryptEvent(const IdIndex &idx, index.session_id, index.sender_key, decryptionResult.error_message.value_or("")); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB " - "access " - "failed.") - .toStdString(); break; case olm::DecryptionErrorCode::DecryptionFailed: nhlog::crypto()->critical( @@ -715,22 +683,8 @@ EventStore::decryptEvent(const IdIndex &idx, index.session_id, index.sender_key, decryptionResult.error_message.value_or("")); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the " - "Olm " - "decrytion returned an error, which is passed as %1.") - .arg( - QString::fromStdString(decryptionResult.error_message.value_or(""))) - .toStdString(); break; case olm::DecryptionErrorCode::ParsingFailed: - dummy.content.body = - tr("-- Encrypted Event (Unknown event type) --", - "Placeholder, when the message was decrypted, but we couldn't parse " - "it, because " - "Nheko/mtxclient don't support that event type yet.") - .toStdString(); break; case olm::DecryptionErrorCode::ReplayAttack: nhlog::crypto()->critical( @@ -738,85 +692,50 @@ EventStore::decryptEvent(const IdIndex &idx, e.event_id, room_id_, index.sender_key); - dummy.content.body = - tr("-- Replay attack! This message index was reused! --").toStdString(); break; - case olm::DecryptionErrorCode::UnknownFingerprint: - // TODO: don't fail, just show in UI. - nhlog::crypto()->critical("Message by unverified fingerprint {}", - index.sender_key); - dummy.content.body = - tr("-- Message by unverified device! --").toStdString(); + case olm::DecryptionErrorCode::NoError: + // unreachable break; } - return asCacheEntry(std::move(dummy)); - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = - olm::client()->decrypt_group_message(session.get(), e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB " - "access " - "failed.") - .toStdString(); - return asCacheEntry(std::move(dummy)); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the " - "Olm " - "decrytion returned an error, which is passed as %1.") - .arg(e.what()) - .toStdString(); - return asCacheEntry(std::move(dummy)); - } - - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = e.event_id; - body["sender"] = e.sender; - body["origin_server_ts"] = e.origin_server_ts; - body["unsigned"] = e.unsigned_data; - - // relations are unencrypted in content... - mtx::common::add_relations(body["content"], e.content.relations); - - json event_array = json::array(); - event_array.push_back(body); - - std::vector temp_events; - mtx::responses::utils::parse_timeline_events(event_array, temp_events); - - if (temp_events.size() == 1) { - auto encInfo = mtx::accessors::file(temp_events[0]); - - if (encInfo) - emit newEncryptedImage(encInfo.value()); - - return asCacheEntry(std::move(temp_events[0])); + return asCacheEntry(std::move(decryptionResult)); } auto encInfo = mtx::accessors::file(decryptionResult.event.value()); if (encInfo) emit newEncryptedImage(encInfo.value()); - return asCacheEntry(std::move(decryptionResult.event.value())); + return asCacheEntry(std::move(decryptionResult)); +} + +void +EventStore::requestSession(const mtx::events::EncryptedEvent &ev, + bool manual) +{ + // we may not want to request keys during initial sync and such + if (suppressKeyRequests) + return; + + // TODO: Look in key backup + auto copy = ev; + copy.room_id = room_id_; + if (pending_key_requests.count(ev.content.session_id)) { + auto &r = pending_key_requests.at(ev.content.session_id); + r.events.push_back(copy); + + // automatically request once every 10 min, manually every 1 min + qint64 delay = manual ? 60 : (60 * 10); + if (r.requested_at + delay < QDateTime::currentSecsSinceEpoch()) { + r.requested_at = QDateTime::currentSecsSinceEpoch(); + olm::send_key_request_for(copy, r.request_id); + } + } else { + PendingKeyRequests request; + request.request_id = "key_request." + http::client()->generate_txn_id(); + request.requested_at = QDateTime::currentSecsSinceEpoch(); + request.events.push_back(copy); + olm::send_key_request_for(copy, request.request_id); + pending_key_requests[ev.content.session_id] = request; + } } void @@ -877,15 +796,56 @@ EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool events_by_id_.insert(index, event_ptr); } - if (decrypt) + if (decrypt) { if (auto encrypted = std::get_if>( - event_ptr)) - return decryptEvent(index, *encrypted); + event_ptr)) { + auto decrypted = decryptEvent(index, *encrypted); + if (decrypted->event) + return &*decrypted->event; + } + } return event_ptr; } +olm::DecryptionErrorCode +EventStore::decryptionError(std::string id) +{ + if (this->thread() != QThread::currentThread()) + nhlog::db()->warn("{} called from a different thread!", __func__); + + if (id.empty()) + return olm::DecryptionErrorCode::NoError; + + IdIndex index{room_id_, std::move(id)}; + auto edits_ = edits(index.id); + if (!edits_.empty()) { + index.id = mtx::accessors::event_id(edits_.back()); + auto event_ptr = + new mtx::events::collections::TimelineEvents(std::move(edits_.back())); + events_by_id_.insert(index, event_ptr); + } + + auto event_ptr = events_by_id_.object(index); + if (!event_ptr) { + auto event = cache::client()->getEvent(room_id_, index.id); + if (!event) { + return olm::DecryptionErrorCode::NoError; + } + event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data)); + events_by_id_.insert(index, event_ptr); + } + + if (auto encrypted = + std::get_if>(event_ptr)) { + auto decrypted = decryptEvent(index, *encrypted); + return decrypted->error; + } + + return olm::DecryptionErrorCode::NoError; +} + void EventStore::fetchMore() { diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h index 7c404102..59c1c7c0 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h @@ -15,6 +15,7 @@ #include #include +#include "Olm.h" #include "Reaction.h" class EventStore : public QObject @@ -78,6 +79,9 @@ public: mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true); QVariantList reactions(const std::string &event_id); + olm::DecryptionErrorCode decryptionError(std::string id); + void requestSession(const mtx::events::EncryptedEvent &ev, + bool manual); int size() const { @@ -119,7 +123,7 @@ public slots: private: std::vector edits(const std::string &event_id); - mtx::events::collections::TimelineEvents *decryptEvent( + olm::DecryptionResult *decryptEvent( const IdIndex &idx, const mtx::events::EncryptedEvent &e); void handle_room_verification(mtx::events::collections::TimelineEvents event); @@ -129,7 +133,7 @@ private: uint64_t first = std::numeric_limits::max(), last = std::numeric_limits::max(); - static QCache decryptedEvents_; + static QCache decryptedEvents_; static QCache events_; static QCache events_by_id_; @@ -137,6 +141,7 @@ private: { std::string request_id; std::vector> events; + qint64 requested_at; }; std::map pending_key_requests; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 10d9788d..99e00a67 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -452,6 +452,7 @@ TimelineModel::roleNames() const {IsEditable, "isEditable"}, {IsEncrypted, "isEncrypted"}, {Trustlevel, "trustlevel"}, + {EncryptionError, "encryptionError"}, {ReplyTo, "replyTo"}, {Reactions, "reactions"}, {RoomId, "roomId"}, @@ -639,6 +640,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return crypto::Trust::Unverified; } + case EncryptionError: + return events.decryptionError(event_id(event)); + case ReplyTo: return QVariant(QString::fromStdString(relations(event).reply_to().value_or(""))); case Reactions: { @@ -690,6 +694,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r m.insert(names[RoomName], data(event, static_cast(RoomName))); m.insert(names[RoomTopic], data(event, static_cast(RoomTopic))); m.insert(names[CallType], data(event, static_cast(CallType))); + m.insert(names[EncryptionError], data(event, static_cast(EncryptionError))); return QVariant(m); } @@ -1551,6 +1556,17 @@ TimelineModel::scrollTimerEvent() } } +void +TimelineModel::requestKeyForEvent(QString id) +{ + auto encrypted_event = events.get(id.toStdString(), "", false); + if (encrypted_event) { + if (auto ev = std::get_if>( + encrypted_event)) + events.requestSession(*ev, true); + } +} + void TimelineModel::copyLinkToEvent(QString eventId) const { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index b5c8ca37..ad7cfbbb 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -212,6 +212,7 @@ public: IsEditable, IsEncrypted, Trustlevel, + EncryptionError, ReplyTo, Reactions, RoomId, @@ -264,6 +265,8 @@ public: endResetModel(); } + Q_INVOKABLE void requestKeyForEvent(QString id); + std::vector<::Reaction> reactions(const std::string &event_id) { auto list = events.reactions(event_id); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 76bc127e..b23ed278 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -157,6 +157,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par 0, "MtxEvent", "Can't instantiate enum!"); + qmlRegisterUncreatableMetaObject( + olm::staticMetaObject, "im.nheko", 1, 0, "Olm", "Can't instantiate enum!"); qmlRegisterUncreatableMetaObject( crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!"); qmlRegisterUncreatableMetaObject(verification::staticMetaObject,