summary refs log tree commit diff
diff options
context:
space:
mode:
authorDeepBlueV7.X <nicolas.werner@hotmail.de>2021-04-27 09:35:47 +0000
committerGitHub <noreply@github.com>2021-04-27 09:35:47 +0000
commit1e3f0f3b269e9813e55d27da27ffe77fecc04b22 (patch)
treebcf9862d3aa1bec082ed80192bd38fab08883df2
parentUpdate translation files (diff)
parentMake forward messages a bit more readable (diff)
downloadnheko-1e3f0f3b269e9813e55d27da27ffe77fecc04b22.tar.xz
Merge pull request #552 from Jedi18/forward_message_feature
Forward Message
-rw-r--r--resources/qml/ForwardCompleter.qml117
-rw-r--r--resources/qml/TimelineView.qml18
-rw-r--r--resources/qml/TopBar.qml4
-rw-r--r--resources/res.qrc1
-rw-r--r--src/EventAccessors.cpp26
-rw-r--r--src/EventAccessors.h26
-rw-r--r--src/Utils.cpp34
-rw-r--r--src/Utils.h9
-rw-r--r--src/timeline/TimelineModel.cpp10
-rw-r--r--src/timeline/TimelineModel.h2
-rw-r--r--src/timeline/TimelineViewManager.cpp132
-rw-r--r--src/timeline/TimelineViewManager.h1
12 files changed, 343 insertions, 37 deletions
diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
new file mode 100644

index 00000000..c544378b --- /dev/null +++ b/resources/qml/ForwardCompleter.qml
@@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./delegates/" +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import im.nheko 1.0 + +Popup { + id: forwardMessagePopup + + property var mid + + function setMessageEventId(mid_in) { + mid = mid_in; + } + + x: Math.round(parent.width / 2 - width / 2) + y: Math.round(parent.height / 2 - height / 2) + modal: true + palette: colors + parent: Overlay.overlay + width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8) + height: implicitHeight + completerPopup.height + padding * 2 + leftPadding: 10 + rightPadding: 10 + background: Rectangle { + color: colors.window + } + + onOpened: { + completerPopup.open(); + roomTextInput.forceActiveFocus(); + } + onClosed: { + completerPopup.close(); + } + + Column { + id: forwardColumn + + spacing: 5 + + Label { + id: titleLabel + + text: qsTr("Forward Message") + font.bold: true + bottomPadding: 10 + color: colors.text + } + + Reply { + id: replyPreview + + modelData: TimelineManager.timeline ? TimelineManager.timeline.getDump(mid, "") : { + } + userColor: TimelineManager.userColor(modelData.userId, colors.window) + } + + MatrixTextField { + id: roomTextInput + + width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 + color: colors.text + onTextEdited: { + completerPopup.completer.searchString = text; + } + Keys.onPressed: { + if (event.key == Qt.Key_Up && completerPopup.opened) { + event.accepted = true; + completerPopup.up(); + } else if (event.key == Qt.Key_Down && completerPopup.opened) { + event.accepted = true; + completerPopup.down(); + } else if (event.matches(StandardKey.InsertParagraphSeparator)) { + completerPopup.finishCompletion(); + event.accepted = true; + } + } + } + + } + + Completer { + id: completerPopup + + y: titleLabel.height + replyPreview.height + roomTextInput.height + roomTextInput.bottomPadding + forwardColumn.spacing * 3 + width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 + completerName: "room" + fullWidth: true + centerRowContent: false + avatarHeight: 24 + avatarWidth: 24 + bottomToTop: false + closePolicy: Popup.NoAutoClose + } + + Connections { + onCompletionSelected: { + TimelineManager.timeline.forwardMessage(messageContextMenu.eventId, id); + forwardMessagePopup.close(); + } + onCountChanged: { + if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count)) + completerPopup.currentIndex = 0; + + } + target: completerPopup + } + + Overlay.modal: Rectangle { + color: Qt.rgba(colors.window.r, colors.window.g, colors.window.b, 0.7) + } + +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 3cc2ab15..81ca7705 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml
@@ -71,6 +71,14 @@ Page { } + Component { + id: forwardCompleterComponent + + ForwardCompleter { + } + + } + Shortcut { sequence: "Ctrl+K" onActivated: { @@ -126,6 +134,16 @@ Page { } Platform.MenuItem { + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage + text: qsTr("Forward") + onTriggered: { + var forwardMess = forwardCompleterComponent.createObject(timelineRoot); + forwardMess.setMessageEventId(messageContextMenu.eventId); + forwardMess.open(); + } + } + + Platform.MenuItem { text: qsTr("Mark as read") } diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index f5c5c84a..858652c2 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml
@@ -72,8 +72,8 @@ Rectangle { font.pointSize: fontMetrics.font.pointSize * 1.1 text: room ? room.roomName : qsTr("No room selected") maximumLineCount: 1 - elide: Text.ElideRight - textFormat: Text.RichText + elide: Text.ElideRight + textFormat: Text.RichText } MatrixText { diff --git a/resources/res.qrc b/resources/res.qrc
index 328f65ca..304493b6 100644 --- a/resources/res.qrc +++ b/resources/res.qrc
@@ -142,6 +142,7 @@ <file>qml/TimelineRow.qml</file> <file>qml/TopBar.qml</file> <file>qml/QuickSwitcher.qml</file> + <file>qml/ForwardCompleter.qml</file> <file>qml/TypingIndicator.qml</file> <file>qml/RoomSettings.qml</file> <file>qml/emoji/EmojiButton.qml</file> diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp
index cfc41a98..362bf4e9 100644 --- a/src/EventAccessors.cpp +++ b/src/EventAccessors.cpp
@@ -11,32 +11,8 @@ #include <type_traits> namespace { -struct nonesuch -{ - ~nonesuch() = delete; - nonesuch(nonesuch const &) = delete; - void operator=(nonesuch const &) = delete; -}; - -namespace detail { -template<class Default, class AlwaysVoid, template<class...> class Op, class... Args> -struct detector -{ - using value_t = std::false_type; - using type = Default; -}; - -template<class Default, template<class...> class Op, class... Args> -struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> -{ - using value_t = std::true_type; - using type = Op<Args...>; -}; - -} // namespace detail - template<template<class...> class Op, class... Args> -using is_detected = typename detail::detector<nonesuch, void, Op, Args...>::value_t; +using is_detected = typename nheko::detail::detector<nheko::nonesuch, void, Op, Args...>::value_t; struct IsStateEvent { diff --git a/src/EventAccessors.h b/src/EventAccessors.h
index ced159c1..a58c7de0 100644 --- a/src/EventAccessors.h +++ b/src/EventAccessors.h
@@ -11,6 +11,32 @@ #include <mtx/events/collections.hpp> +namespace nheko { +struct nonesuch +{ + ~nonesuch() = delete; + nonesuch(nonesuch const &) = delete; + void operator=(nonesuch const &) = delete; +}; + +namespace detail { +template<class Default, class AlwaysVoid, template<class...> class Op, class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template<class Default, template<class...> class Op, class... Args> +struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op<Args...>; +}; + +} // namespace detail +} + namespace mtx::accessors { std::string event_id(const mtx::events::collections::TimelineEvents &event); diff --git a/src/Utils.cpp b/src/Utils.cpp
index bc5b72cb..a8e13521 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp
@@ -52,6 +52,28 @@ createDescriptionInfo(const Event &event, const QString &localUser, const QStrin ts}; } +std::string +utils::stripReplyFromBody(const std::string &bodyi) +{ + QString body = QString::fromStdString(bodyi); + QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption); + while (body.startsWith(">")) + body.remove(plainQuote); + if (body.startsWith("\n")) + body.remove(0, 1); + return body.toStdString(); +} + +std::string +utils::stripReplyFromFormattedBody(const std::string &formatted_bodyi) +{ + QString formatted_body = QString::fromStdString(formatted_bodyi); + formatted_body.remove(QRegularExpression("<mx-reply>.*</mx-reply>", + QRegularExpression::DotMatchesEverythingOption)); + formatted_body.replace("@room", "@\u2060aroom"); + return formatted_body.toStdString(); +} + RelatedInfo utils::stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_) { @@ -63,19 +85,15 @@ utils::stripReplyFallbacks(const TimelineEvent &event, std::string id, QString r // get body, strip reply fallback, then transform the event to text, if it is a media event // etc related.quoted_body = QString::fromStdString(mtx::accessors::body(event)); - QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption); - while (related.quoted_body.startsWith(">")) - related.quoted_body.remove(plainQuote); - if (related.quoted_body.startsWith("\n")) - related.quoted_body.remove(0, 1); + related.quoted_body = + QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString())); related.quoted_body = utils::getQuoteBody(related); related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room")); // get quoted body and strip reply fallback related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event); - related.quoted_formatted_body.remove(QRegularExpression( - "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption)); - related.quoted_formatted_body.replace("@room", "@\u2060aroom"); + related.quoted_formatted_body = QString::fromStdString( + stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString())); related.room = room_id_; return related; diff --git a/src/Utils.h b/src/Utils.h
index 7a9eb777..e976cf81 100644 --- a/src/Utils.h +++ b/src/Utils.h
@@ -9,6 +9,7 @@ #include <QCoreApplication> #include <QDateTime> #include <QPixmap> +#include <QRegularExpression> #include <mtx/events/collections.hpp> #include <mtx/events/common.hpp> @@ -40,6 +41,14 @@ namespace utils { using TimelineEvent = mtx::events::collections::TimelineEvents; +//! Helper function to remove reply fallback from body +std::string +stripReplyFromBody(const std::string &body); + +//! Helper function to remove reply fallback from formatted body +std::string +stripReplyFromFormattedBody(const std::string &formatted_body); + RelatedInfo stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index bfd95b0d..30ce176e 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -827,6 +827,16 @@ TimelineModel::viewRawMessage(QString id) const } void +TimelineModel::forwardMessage(QString eventId, QString roomId) +{ + auto e = events.get(eventId.toStdString(), ""); + if (!e) + return; + + emit forwardToRoom(e, roomId); +} + +void TimelineModel::viewDecryptedRawMessage(QString id) const { auto e = events.get(id.toStdString(), ""); diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 06da95c6..fbe963d2 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h
@@ -219,6 +219,7 @@ public: Q_INVOKABLE QString formatPowerLevelEvent(QString id); Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void forwardMessage(QString eventId, QString roomId); Q_INVOKABLE void viewDecryptedRawMessage(QString id) const; Q_INVOKABLE void openUserProfile(QString userid, bool global = false); Q_INVOKABLE void openRoomSettings(); @@ -322,6 +323,7 @@ signals: void roomNameChanged(); void roomTopicChanged(); void roomAvatarUrlChanged(); + void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); private: template<typename T> diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index e9986c7e..9d12825f 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -18,6 +18,7 @@ #include "CompletionProxyModel.h" #include "DelegateChooser.h" #include "DeviceVerificationFlow.h" +#include "EventAccessors.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -31,13 +32,59 @@ #include "ui/NhekoCursorShape.h" #include "ui/NhekoDropArea.h" -#include <iostream> //only for debugging - Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) Q_DECLARE_METATYPE(std::vector<DeviceInfo>) namespace msgs = mtx::events::msg; +namespace { +template<template<class...> class Op, class... Args> +using is_detected = typename nheko::detail::detector<nheko::nonesuch, void, Op, Args...>::value_t; + +template<class Content> +using file_t = decltype(Content::file); + +template<class Content> +using url_t = decltype(Content::url); + +template<class Content> +using body_t = decltype(Content::body); + +template<class Content> +using formatted_body_t = decltype(Content::formatted_body); + +template<typename T> +static constexpr bool +messageWithFileAndUrl(const mtx::events::Event<T> &) +{ + return is_detected<file_t, T>::value && is_detected<url_t, T>::value; +} + +template<typename T> +static constexpr void +removeReplyFallback(mtx::events::Event<T> &e) +{ + if constexpr (is_detected<body_t, T>::value) { + if constexpr (std::is_same_v<std::optional<std::string>, + std::remove_cv_t<decltype(e.content.body)>>) { + if (e.content.body) { + e.content.body = utils::stripReplyFromBody(e.content.body); + } + } else if constexpr (std::is_same_v<std::string, + std::remove_cv_t<decltype(e.content.body)>>) { + e.content.body = utils::stripReplyFromBody(e.content.body); + } + } + + if constexpr (is_detected<formatted_body_t, T>::value) { + if (e.content.format == "org.matrix.custom.html") { + e.content.formatted_body = + utils::stripReplyFromFormattedBody(e.content.formatted_body); + } + } +} +} + void TimelineViewManager::updateEncryptedDescriptions() { @@ -329,6 +376,10 @@ TimelineViewManager::addRoom(const QString &room_id) &TimelineModel::newEncryptedImage, imgProvider, &MxcImageProvider::addEncryptionInfo); + connect(newRoom.data(), + &TimelineModel::forwardToRoom, + this, + &TimelineViewManager::forwardMessageToRoom); models.insert(room_id, std::move(newRoom)); } } @@ -616,3 +667,80 @@ TimelineViewManager::focusTimeline() { getWidget()->setFocus(); } + +void +TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, + QString roomId) +{ + auto room = models.find(roomId); + auto content = mtx::accessors::url(*e); + std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e); + + if (encryptionInfo) { + http::client()->download( + content, + [this, roomId, e, encryptionInfo](const std::string &res, + const std::string &content_type, + const std::string &originalFilename, + mtx::http::RequestErr err) { + if (err) + return; + + auto data = mtx::crypto::to_string( + mtx::crypto::decrypt_file(res, encryptionInfo.value())); + + http::client()->upload( + data, + content_type, + originalFilename, + [this, roomId, e](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) mutable { + if (err) { + nhlog::net()->warn("failed to upload media: {} {} ({})", + err->matrix_error.error, + to_string(err->matrix_error.errcode), + static_cast<int>(err->status_code)); + return; + } + + std::visit( + [this, roomId, url = res.content_uri](auto ev) { + if constexpr (mtx::events::message_content_to_type< + decltype(ev.content)> == + mtx::events::EventType::RoomMessage) { + if constexpr (messageWithFileAndUrl(ev)) { + ev.content.relations.relations + .clear(); + ev.content.file.reset(); + ev.content.url = url; + } + + auto room = models.find(roomId); + removeReplyFallback(ev); + ev.content.relations.relations.clear(); + room.value()->sendMessageEvent( + ev.content, + mtx::events::EventType::RoomMessage); + } + }, + *e); + }); + + return; + }); + + return; + } + + std::visit( + [room](auto e) { + if constexpr (mtx::events::message_content_to_type<decltype(e.content)> == + mtx::events::EventType::RoomMessage) { + e.content.relations.relations.clear(); + removeReplyFallback(e); + room.value()->sendMessageEvent(e.content, + mtx::events::EventType::RoomMessage); + } + }, + *e); +} diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 3b405142..9703ee56 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h
@@ -146,6 +146,7 @@ public slots: void backToRooms() { emit showRoomList(); } QObject *completerFor(QString completerName, QString roomId = ""); + void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); private slots: void openImageOverlayInternal(QString eventId, QImage img);