diff --git a/imports/NeoChat/Component/Timeline/LinkPreviewDelegate.qml b/imports/NeoChat/Component/Timeline/LinkPreviewDelegate.qml new file mode 100644 index 000000000..4e170a227 --- /dev/null +++ b/imports/NeoChat/Component/Timeline/LinkPreviewDelegate.qml @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2022 Bharadwaj Raju +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.15 as Kirigami + +import org.kde.neochat 1.0 + +RowLayout { + id: row + property var links: model.display.match(/(\bhttps?:\/\/[^\s\<\>\"\']*[^\s\<\>\"\'])/g) + // don't show previews for room links or user mentions + .filter(link => !link.includes("https://matrix.to")) + // remove ending fullstops and commas + .map(link => (link.length && [".", ","].includes(link[link.length-1])) ? link.substring(0, link.length-1) : link) + LinkPreviewer { + id: lp + url: links[0] + } + visible: lp.loaded && lp.title + Rectangle { + Layout.fillHeight: true + width: Kirigami.Units.smallSpacing + visible: lp.loaded && lp.title + color: Kirigami.Theme.highlightColor + } + Image { + visible: lp.imageSource + Layout.maximumHeight: Kirigami.Units.gridUnit * 5 + Layout.maximumWidth: Kirigami.Units.gridUnit * 5 + source: lp.imageSource.replace("mxc://", "image://mxc/") + fillMode: Image.PreserveAspectFit + } + ColumnLayout { + id: column + spacing: Kirigami.Units.smallSpacing + Kirigami.Heading { + Layout.maximumWidth: messageDelegate.bubbleMaxWidth + Layout.fillWidth: true + level: 4 + wrapMode: Text.Wrap + textFormat: Text.RichText + text: " + " + lp.title.replace("–", "—") + "" + visible: lp.loaded + onLinkActivated: RoomManager.openResource(link) + } + Label { + text: lp.description + Layout.maximumWidth: messageDelegate.bubbleMaxWidth + Layout.fillWidth: true + wrapMode: Text.Wrap + visible: lp.loaded && lp.description + } + } +} + diff --git a/imports/NeoChat/Component/Timeline/MessageDelegate.qml b/imports/NeoChat/Component/Timeline/MessageDelegate.qml index ad9cae648..8d40c6cc7 100644 --- a/imports/NeoChat/Component/Timeline/MessageDelegate.qml +++ b/imports/NeoChat/Component/Timeline/MessageDelegate.qml @@ -19,9 +19,22 @@ TimelineContainer { onReplyClicked: ListView.view.goToEvent(eventID) hoverComponent: hoverActions - innerObject: RichLabel { - id: label - isEmote: messageDelegate.isEmote - Layout.maximumWidth: messageDelegate.bubbleMaxWidth + innerObject: ColumnLayout { + RichLabel { + id: label + isEmote: messageDelegate.isEmote + Layout.maximumWidth: messageDelegate.bubbleMaxWidth + } + Loader { + id: linkPreviewLoader + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.leftMargin: Kirigami.Units.largeSpacing + height: active ? item.implicitHeight : 0 + active: !currentRoom.usesEncryption && model.display && model.display.includes("http") + visible: active + sourceComponent: LinkPreviewDelegate { + anchors.verticalCenter: parent.verticalCenter + } + } } } diff --git a/imports/NeoChat/Component/Timeline/qmldir b/imports/NeoChat/Component/Timeline/qmldir index e1691676e..f2a1d5113 100644 --- a/imports/NeoChat/Component/Timeline/qmldir +++ b/imports/NeoChat/Component/Timeline/qmldir @@ -12,3 +12,4 @@ EncryptedDelegate 1.0 EncryptedDelegate.qml EventDelegate 1.0 EventDelegate.qml MessageDelegate 1.0 MessageDelegate.qml ReadMarkerDelegate 1.0 ReadMarkerDelegate.qml +LinkPreviewDelegate 1.0 LinkPreviewDelegate.qml diff --git a/res.qrc b/res.qrc index 56703afba..1de871c30 100644 --- a/res.qrc +++ b/res.qrc @@ -40,6 +40,7 @@ imports/NeoChat/Component/Timeline/SectionDelegate.qml imports/NeoChat/Component/Timeline/VideoDelegate.qml imports/NeoChat/Component/Timeline/ReactionDelegate.qml + imports/NeoChat/Component/Timeline/LinkPreviewDelegate.qml imports/NeoChat/Component/Timeline/AudioDelegate.qml imports/NeoChat/Component/Timeline/FileDelegate.qml imports/NeoChat/Component/Timeline/ImageDelegate.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 06d152d9b..4b70f35ef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,6 +39,7 @@ add_library(neochat STATIC collapsestateproxymodel.cpp urlhelper.cpp windowcontroller.cpp + linkpreviewer.cpp ) add_executable(neochat-app diff --git a/src/linkpreviewer.cpp b/src/linkpreviewer.cpp new file mode 100644 index 000000000..4cb62d7fa --- /dev/null +++ b/src/linkpreviewer.cpp @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2022 Bharadwaj Raju +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL + +#include "linkpreviewer.h" + +#include "controller.h" + +LinkPreviewer::LinkPreviewer(QObject *parent) + : QObject(parent) + , m_loaded(false) +{ +} + +bool LinkPreviewer::loaded() const +{ + return m_loaded; +} + +QString LinkPreviewer::title() const +{ + return m_title; +} + +QString LinkPreviewer::description() const +{ + return m_description; +} + +QString LinkPreviewer::imageSource() const +{ + return m_imageSource; +} + +QUrl LinkPreviewer::url() const +{ + return m_url; +} + +void LinkPreviewer::setUrl(QUrl url) +{ + if (url.scheme() == QStringLiteral("https")) { + m_loaded = false; + Q_EMIT loadedChanged(); + + m_url = url; + Q_EMIT urlChanged(); + + auto conn = Controller::instance().activeConnection(); + + GetUrlPreviewJob *job = conn->callApi(m_url.toString()); + + connect(job, &BaseJob::success, this, [this, job]() { + const auto json = job->jsonData(); + m_title = json["og:title"].toString(); + m_description = json["og:description"].toString(); + m_imageSource = json["og:image"].toString(); + m_loaded = true; + Q_EMIT titleChanged(); + Q_EMIT descriptionChanged(); + Q_EMIT imageSourceChanged(); + Q_EMIT loadedChanged(); + }); + } +} diff --git a/src/linkpreviewer.h b/src/linkpreviewer.h new file mode 100644 index 000000000..44992e1f9 --- /dev/null +++ b/src/linkpreviewer.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2022 Bharadwaj Raju +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include + +#include + +using namespace Quotient; + +class LinkPreviewer : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged) + Q_PROPERTY(QString title READ title NOTIFY titleChanged) + Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) + Q_PROPERTY(QString imageSource READ imageSource NOTIFY imageSourceChanged) + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + +public: + explicit LinkPreviewer(QObject *parent = nullptr); + + [[nodiscard]] bool loaded() const; + [[nodiscard]] QString title() const; + [[nodiscard]] QString description() const; + [[nodiscard]] QString imageSource() const; + [[nodiscard]] QUrl url() const; + + void setUrl(QUrl); + +private: + bool m_loaded; + QString m_title; + QString m_description; + QString m_imageSource; + QUrl m_url; + +Q_SIGNALS: + void loadedChanged(); + void titleChanged(); + void descriptionChanged(); + void imageSourceChanged(); + void urlChanged(); +}; diff --git a/src/main.cpp b/src/main.cpp index 64bcf1d10..cf3dd2264 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -59,6 +59,7 @@ #include "emojimodel.h" #include "filetypesingleton.h" #include "joinrulesevent.h" +#include "linkpreviewer.h" #include "login.h" #include "matriximageprovider.h" #include "messageeventmodel.h" @@ -213,6 +214,7 @@ int main(int argc, char *argv[]) qmlRegisterType("org.kde.neochat", 1, 0, "SortFilterRoomListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "SortFilterSpaceListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "DevicesModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "LinkPreviewer"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM");