diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 481561d2..2b6a8f26 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -79,6 +79,78 @@ Page { } + Component { + id: forwardCompleter + + Popup { + id: forwardMessagePopup + x: 400 + y: 400 + + property var mid + + onOpened: { + completerPopup.open(); + roomTextInput.forceActiveFocus(); + } + + background: Rectangle { + border.color: "#444" + } + + function setMessageEventId(mid_in) { + mid = mid_in; + } + + MatrixTextField { + id: roomTextInput + + width: 100 + + 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: 50 + width: 100 + completerName: "room" + 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 + } + } + } + Shortcut { sequence: "Ctrl+K" onActivated: { @@ -133,6 +205,15 @@ Page { onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId) } + Platform.MenuItem { + text: qsTr("Forward") + onTriggered: { + var forwardMess = forwardCompleter.createObject(timelineRoot); + forwardMess.open(); + forwardMess.setMessageEventId(messageContextMenu.eventId) + } + } + Platform.MenuItem { text: qsTr("Mark as read") } diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 9db16bae..d4fcfacf 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -41,6 +41,14 @@ public: connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping); } + void image(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions, + const QString &blurhash); + public slots: QString text() const; QString previousText(); @@ -70,13 +78,6 @@ private: void emote(QString body, bool rainbowify); void notice(QString body, bool rainbowify); void command(QString name, QString args); - void image(const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions, - const QString &blurhash); void file(const QString &filename, const std::optional &encryptedFile, const QString &url, diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 8e96cb3e..e3efe5ad 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -822,6 +822,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, cache::isRoomEncrypted(room_id_.toStdString())); +} + void TimelineModel::viewDecryptedRawMessage(QString id) const { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 06da95c6..3e6f6f15 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,9 @@ signals: void roomNameChanged(); void roomTopicChanged(); void roomAvatarUrlChanged(); + void forwardToRoom(mtx::events::collections::TimelineEvents *e, + QString roomId, + bool sentFromEncrypted); private: template diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index f15b0b14..ae807f2d 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -4,6 +4,7 @@ #include "TimelineViewManager.h" +#include #include #include #include @@ -25,14 +26,13 @@ #include "RoomsModel.h" #include "UserSettingsPage.h" #include "UsersModel.h" +#include "blurhash.hpp" #include "dialogs/ImageOverlay.h" #include "emoji/EmojiModel.h" #include "emoji/Provider.h" #include "ui/NhekoCursorShape.h" #include "ui/NhekoDropArea.h" -#include //only for debugging - Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) Q_DECLARE_METATYPE(std::vector) @@ -332,6 +332,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)); } } @@ -614,3 +618,98 @@ TimelineViewManager::focusTimeline() { getWidget()->setFocus(); } + +void +TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, + QString roomId, + bool sentFromEncrypted) +{ + auto elem = *e; + auto room = models.find(roomId); + auto messageType = mtx::accessors::msg_type(elem); + + if (sentFromEncrypted && messageType == mtx::events::MessageType::Image) { + auto body = mtx::accessors::body(elem); + auto mimetype = mtx::accessors::mimetype(elem); + auto imageHeight = mtx::accessors::media_height(elem); + auto imageWidth = mtx::accessors::media_height(elem); + + QString mxcUrl = QString::fromStdString(mtx::accessors::url(elem)); + MxcImageProvider::download( + mxcUrl.remove("mxc://"), + QSize(imageWidth, imageHeight), + [this, roomId, body, mimetype](QString, QSize, QImage image, QString) { + QByteArray data = + QByteArray::fromRawData((const char *)image.bits(), image.byteCount()); + + auto payload = std::string(data.data(), data.size()); + std::optional encryptedFile; + + QSize dimensions; + QString blurhash; + auto mimeClass = QString::fromStdString(mimetype).split("/")[0]; + + dimensions = image.size(); + if (image.height() > 200 && image.width() > 360) + image = image.scaled(360, 200, Qt::KeepAspectRatioByExpanding); + std::vector data_; + for (int y = 0; y < image.height(); y++) { + for (int x = 0; x < image.width(); x++) { + auto p = image.pixel(x, y); + data_.push_back(static_cast(qRed(p))); + data_.push_back(static_cast(qGreen(p))); + data_.push_back(static_cast(qBlue(p))); + } + } + blurhash = QString::fromStdString( + blurhash::encode(data_.data(), image.width(), image.height(), 4, 3)); + + http::client()->upload( + payload, + encryptedFile ? "application/octet-stream" : mimetype, + body, + [this, + roomId, + filename = body, + encryptedFile = std::move(encryptedFile), + mimeClass, + mimetype, + size = payload.size(), + dimensions, + blurhash](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; + } + + auto url = QString::fromStdString(res.content_uri); + if (encryptedFile) + encryptedFile->url = res.content_uri; + + auto r = models.find(roomId); + r.value()->input()->image(QString::fromStdString(filename), + encryptedFile, + url, + QString::fromStdString(mimetype), + size, + dimensions, + blurhash); + }); + }); + return; + }; + + std::visit( + [room](auto e) { + if constexpr (mtx::events::message_content_to_type == + mtx::events::EventType::RoomMessage) { + room.value()->sendMessageEvent(e.content, + mtx::events::EventType::RoomMessage); + } + }, + elem); +} \ No newline at end of file diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 3b405142..40bee990 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,6 +147,9 @@ public slots: void backToRooms() { emit showRoomList(); } QObject *completerFor(QString completerName, QString roomId = ""); + void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, + QString roomId, + bool sentFromEncrypted); private slots: void openImageOverlayInternal(QString eventId, QImage img);