Merge pull request #552 from Jedi18/forward_message_feature

Forward Message
This commit is contained in:
DeepBlueV7.X 2021-04-27 09:35:47 +00:00 committed by GitHub
commit 1e3f0f3b26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 343 additions and 37 deletions

View file

@ -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)
}
}

View file

@ -71,6 +71,14 @@ Page {
} }
Component {
id: forwardCompleterComponent
ForwardCompleter {
}
}
Shortcut { Shortcut {
sequence: "Ctrl+K" sequence: "Ctrl+K"
onActivated: { onActivated: {
@ -125,6 +133,16 @@ Page {
onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId) 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 { Platform.MenuItem {
text: qsTr("Mark as read") text: qsTr("Mark as read")
} }

View file

@ -72,8 +72,8 @@ Rectangle {
font.pointSize: fontMetrics.font.pointSize * 1.1 font.pointSize: fontMetrics.font.pointSize * 1.1
text: room ? room.roomName : qsTr("No room selected") text: room ? room.roomName : qsTr("No room selected")
maximumLineCount: 1 maximumLineCount: 1
elide: Text.ElideRight elide: Text.ElideRight
textFormat: Text.RichText textFormat: Text.RichText
} }
MatrixText { MatrixText {

View file

@ -142,6 +142,7 @@
<file>qml/TimelineRow.qml</file> <file>qml/TimelineRow.qml</file>
<file>qml/TopBar.qml</file> <file>qml/TopBar.qml</file>
<file>qml/QuickSwitcher.qml</file> <file>qml/QuickSwitcher.qml</file>
<file>qml/ForwardCompleter.qml</file>
<file>qml/TypingIndicator.qml</file> <file>qml/TypingIndicator.qml</file>
<file>qml/RoomSettings.qml</file> <file>qml/RoomSettings.qml</file>
<file>qml/emoji/EmojiButton.qml</file> <file>qml/emoji/EmojiButton.qml</file>

View file

@ -11,32 +11,8 @@
#include <type_traits> #include <type_traits>
namespace { 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> 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 struct IsStateEvent
{ {

View file

@ -11,6 +11,32 @@
#include <mtx/events/collections.hpp> #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 { namespace mtx::accessors {
std::string std::string
event_id(const mtx::events::collections::TimelineEvents &event); event_id(const mtx::events::collections::TimelineEvents &event);

View file

@ -52,6 +52,28 @@ createDescriptionInfo(const Event &event, const QString &localUser, const QStrin
ts}; 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 RelatedInfo
utils::stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_) 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 // get body, strip reply fallback, then transform the event to text, if it is a media event
// etc // etc
related.quoted_body = QString::fromStdString(mtx::accessors::body(event)); related.quoted_body = QString::fromStdString(mtx::accessors::body(event));
QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption); related.quoted_body =
while (related.quoted_body.startsWith(">")) QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString()));
related.quoted_body.remove(plainQuote);
if (related.quoted_body.startsWith("\n"))
related.quoted_body.remove(0, 1);
related.quoted_body = utils::getQuoteBody(related); related.quoted_body = utils::getQuoteBody(related);
related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room")); related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room"));
// get quoted body and strip reply fallback // get quoted body and strip reply fallback
related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event); related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
related.quoted_formatted_body.remove(QRegularExpression( related.quoted_formatted_body = QString::fromStdString(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption)); stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString()));
related.quoted_formatted_body.replace("@room", "@\u2060aroom");
related.room = room_id_; related.room = room_id_;
return related; return related;

View file

@ -9,6 +9,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDateTime> #include <QDateTime>
#include <QPixmap> #include <QPixmap>
#include <QRegularExpression>
#include <mtx/events/collections.hpp> #include <mtx/events/collections.hpp>
#include <mtx/events/common.hpp> #include <mtx/events/common.hpp>
@ -40,6 +41,14 @@ namespace utils {
using TimelineEvent = mtx::events::collections::TimelineEvents; 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 RelatedInfo
stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_); stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_);

View file

@ -826,6 +826,16 @@ TimelineModel::viewRawMessage(QString id) const
Q_UNUSED(dialog); Q_UNUSED(dialog);
} }
void
TimelineModel::forwardMessage(QString eventId, QString roomId)
{
auto e = events.get(eventId.toStdString(), "");
if (!e)
return;
emit forwardToRoom(e, roomId);
}
void void
TimelineModel::viewDecryptedRawMessage(QString id) const TimelineModel::viewDecryptedRawMessage(QString id) const
{ {

View file

@ -219,6 +219,7 @@ public:
Q_INVOKABLE QString formatPowerLevelEvent(QString id); Q_INVOKABLE QString formatPowerLevelEvent(QString id);
Q_INVOKABLE void viewRawMessage(QString id) const; 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 viewDecryptedRawMessage(QString id) const;
Q_INVOKABLE void openUserProfile(QString userid, bool global = false); Q_INVOKABLE void openUserProfile(QString userid, bool global = false);
Q_INVOKABLE void openRoomSettings(); Q_INVOKABLE void openRoomSettings();
@ -322,6 +323,7 @@ signals:
void roomNameChanged(); void roomNameChanged();
void roomTopicChanged(); void roomTopicChanged();
void roomAvatarUrlChanged(); void roomAvatarUrlChanged();
void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
private: private:
template<typename T> template<typename T>

View file

@ -18,6 +18,7 @@
#include "CompletionProxyModel.h" #include "CompletionProxyModel.h"
#include "DelegateChooser.h" #include "DelegateChooser.h"
#include "DeviceVerificationFlow.h" #include "DeviceVerificationFlow.h"
#include "EventAccessors.h"
#include "Logging.h" #include "Logging.h"
#include "MainWindow.h" #include "MainWindow.h"
#include "MatrixClient.h" #include "MatrixClient.h"
@ -31,13 +32,59 @@
#include "ui/NhekoCursorShape.h" #include "ui/NhekoCursorShape.h"
#include "ui/NhekoDropArea.h" #include "ui/NhekoDropArea.h"
#include <iostream> //only for debugging
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
Q_DECLARE_METATYPE(std::vector<DeviceInfo>) Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
namespace msgs = mtx::events::msg; 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 void
TimelineViewManager::updateEncryptedDescriptions() TimelineViewManager::updateEncryptedDescriptions()
{ {
@ -329,6 +376,10 @@ TimelineViewManager::addRoom(const QString &room_id)
&TimelineModel::newEncryptedImage, &TimelineModel::newEncryptedImage,
imgProvider, imgProvider,
&MxcImageProvider::addEncryptionInfo); &MxcImageProvider::addEncryptionInfo);
connect(newRoom.data(),
&TimelineModel::forwardToRoom,
this,
&TimelineViewManager::forwardMessageToRoom);
models.insert(room_id, std::move(newRoom)); models.insert(room_id, std::move(newRoom));
} }
} }
@ -616,3 +667,80 @@ TimelineViewManager::focusTimeline()
{ {
getWidget()->setFocus(); 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);
}

View file

@ -146,6 +146,7 @@ public slots:
void backToRooms() { emit showRoomList(); } void backToRooms() { emit showRoomList(); }
QObject *completerFor(QString completerName, QString roomId = ""); QObject *completerFor(QString completerName, QString roomId = "");
void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
private slots: private slots:
void openImageOverlayInternal(QString eventId, QImage img); void openImageOverlayInternal(QString eventId, QImage img);