From 4dd994ae009b622cd35e292d1170a3f60a26c4d6 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 23 Jul 2021 18:11:33 -0400 Subject: QML the read receipts list There are probably a few things wrong with this, but I'm going to call it good enough for an initial commit --- src/timeline/TimelineModel.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/timeline/TimelineModel.cpp') 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 -- cgit 1.4.1 From 7e538851d6e3779434722e56a968e9f8b8a9da0d Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 28 Jul 2021 21:31:37 -0400 Subject: Use a QSortFilterProxyModel instead of resetting the model --- CMakeLists.txt | 4 +-- resources/qml/ReadReceipts.qml | 4 +-- src/ReadReceiptsModel.cpp | 53 +++++++++++++++++++++++++----------- src/ReadReceiptsModel.h | 27 ++++++++++++++++-- src/timeline/TimelineModel.cpp | 2 +- src/timeline/TimelineModel.h | 2 +- src/timeline/TimelineViewManager.cpp | 6 ++-- 7 files changed, 70 insertions(+), 28 deletions(-) (limited to 'src/timeline/TimelineModel.cpp') 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); - std::sort(readReceipts_.begin(), - readReceipts_.end(), - [](const QPair &a, const QPair &b) { - return a.second > b.second; - }); + for (const auto &user : users) { + QPair item = { + QString::fromStdString(user.second), + QDateTime::fromMSecsSinceEpoch(user.first)}; + if (!readReceipts_.contains(item)) + readReceipts_.push_back(item); + } - endResetModel(); + endInsertRows(); + } } 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( -- cgit 1.4.1 From dab1c9068ac6d48a1faba54d7510deb360ae74e3 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 30 Jul 2021 22:13:58 -0400 Subject: 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 (limited to 'src/timeline/TimelineModel.cpp') 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; -- cgit 1.4.1 From a57a15a2e07da8cc07bc12e828b7c636efe36cbc Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 6 Aug 2021 01:45:47 +0200 Subject: 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 ++++++++++ resources/qml/dialogs/ImagePackEditorDialog.qml | 283 ++++++++++++++++++++++ resources/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 (limited to 'src/timeline/TimelineModel.cpp') 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); -- cgit 1.4.1 From 72bbad7485db6ac1803f81344c29b93d9fa70945 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Aug 2021 22:51:09 +0200 Subject: 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 (limited to 'src/timeline/TimelineModel.cpp') 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, -- cgit 1.4.1