diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
new file mode 100644
index 00000000..f85362c1
--- /dev/null
+++ b/resources/qml/ForwardCompleter.qml
@@ -0,0 +1,112 @@
+// 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
+
+ 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 >= 300 ? implicitWidth : 300
+ height: implicitHeight + completerPopup.height + padding * 2
+ leftPadding: 10
+ rightPadding: 10
+
+ property var mid
+
+ onOpened: {
+ completerPopup.open();
+ roomTextInput.forceActiveFocus();
+ }
+
+ onClosed: {
+ completerPopup.close();
+ }
+
+ function setMessageEventId(mid_in) {
+ mid = mid_in;
+ }
+
+ Column {
+ id: forwardColumn
+
+ spacing: 5
+
+ Label {
+ id: titleLabel
+
+ text: qsTr("Forward Message")
+ font.bold: true
+ bottomPadding: 10
+ }
+
+ 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: "#aa1E1E1E"
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 3cc2ab15..7cf029e1 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -71,6 +71,13 @@ Page {
}
+ Component {
+ id: forwardCompleterComponent
+
+ ForwardCompleter {
+ }
+ }
+
Shortcut {
sequence: "Ctrl+K"
onActivated: {
@@ -125,6 +132,16 @@ Page {
onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId)
}
+ 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/res.qrc b/resources/res.qrc
index 328f65ca..304493b6 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -142,6 +142,7 @@
qml/TimelineRow.qml
qml/TopBar.qml
qml/QuickSwitcher.qml
+ qml/ForwardCompleter.qml
qml/TypingIndicator.qml
qml/RoomSettings.qml
qml/emoji/EmojiButton.qml
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
namespace {
-struct nonesuch
-{
- ~nonesuch() = delete;
- nonesuch(nonesuch const &) = delete;
- void operator=(nonesuch const &) = delete;
-};
-
-namespace detail {
-template class Op, class... Args>
-struct detector
-{
- using value_t = std::false_type;
- using type = Default;
-};
-
-template class Op, class... Args>
-struct detector>, Op, Args...>
-{
- using value_t = std::true_type;
- using type = Op;
-};
-
-} // namespace detail
-
template class Op, class... Args>
-using is_detected = typename detail::detector::value_t;
+using is_detected = typename nheko::detail::detector::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
+namespace nheko {
+struct nonesuch
+{
+ ~nonesuch() = delete;
+ nonesuch(nonesuch const &) = delete;
+ void operator=(nonesuch const &) = delete;
+};
+
+namespace detail {
+template class Op, class... Args>
+struct detector
+{
+ using value_t = std::false_type;
+ using type = Default;
+};
+
+template class Op, class... Args>
+struct detector>, Op, Args...>
+{
+ using value_t = std::true_type;
+ using type = Op;
+};
+
+} // 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(".*",
+ 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(
- ".*", 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
#include
#include
+#include
#include
#include
@@ -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
@@ -826,6 +826,16 @@ TimelineModel::viewRawMessage(QString id) const
Q_UNUSED(dialog);
}
+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
{
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
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index e9986c7e..518ccaf1 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -31,8 +31,6 @@
#include "ui/NhekoCursorShape.h"
#include "ui/NhekoDropArea.h"
-#include //only for debugging
-
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
Q_DECLARE_METATYPE(std::vector)
@@ -329,6 +327,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 +618,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 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(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 ==
+ mtx::events::EventType::RoomMessage) {
+ e.content.relations.relations.clear();
+ removeReplyFallback(e);
+ room.value()->sendMessageEvent(e.content,
+ mtx::events::EventType::RoomMessage);
+ }
+ },
+ *e);
+}
\ No newline at end of file
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 3b405142..809b286e 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -16,6 +16,7 @@
#include "Cache.h"
#include "CallManager.h"
+#include "EventAccessors.h"
#include "Logging.h"
#include "TimelineModel.h"
#include "Utils.h"
@@ -146,10 +147,58 @@ 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);
+private:
+ template class Op, class... Args>
+ using is_detected =
+ typename nheko::detail::detector::value_t;
+
+ template
+ using file_t = decltype(Content::file);
+
+ template
+ using url_t = decltype(Content::url);
+
+ template
+ using body_t = decltype(Content::body);
+
+ template
+ using formatted_body_t = decltype(Content::formatted_body);
+
+ template
+ static constexpr bool messageWithFileAndUrl(const mtx::events::Event &)
+ {
+ return is_detected::value && is_detected::value;
+ }
+
+ template
+ static constexpr void removeReplyFallback(mtx::events::Event &e)
+ {
+ if constexpr (is_detected::value) {
+ if constexpr (std::is_same_v,
+ std::remove_cv_t>) {
+ 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>) {
+ e.content.body = utils::stripReplyFromBody(e.content.body);
+ }
+ }
+
+ if constexpr (is_detected::value) {
+ if (e.content.format == "org.matrix.custom.html") {
+ e.content.formatted_body =
+ utils::stripReplyFromFormattedBody(e.content.formatted_body);
+ }
+ }
+ }
+
private:
#ifdef USE_QUICK_VIEW
QQuickView *view;