Refactor LinkPreviewer

Refactor `LinkPreviewer` to take an event and put the functions for getting the link in the class itself. This means the functions in `EventHandler` are no longer required.

This mr also sets up `LinkPreviewer` so that it is automatically updated when an event is edited. This includes changing the link if edited, and it can handle a message having a previous link removed or a one added when one didn't exist before.

Also adds test suite.
This commit is contained in:
James Graham
2024-01-07 18:07:13 +00:00
parent f361f4e2d8
commit 51f7de117d
22 changed files with 401 additions and 194 deletions

View File

@@ -777,43 +777,6 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
return mediaInfo;
}
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getLinkPreviewer called with m_room set to nullptr.";
return nullptr;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLinkPreviewer called with m_event set to nullptr.";
return nullptr;
}
if (!m_event->is<RoomMessageEvent>()) {
return nullptr;
}
QString text;
auto event = eventCast<const RoomMessageEvent>(m_event);
if (event->hasTextContent()) {
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
if (textContent) {
text = textContent->body;
} else {
text = event->plainBody();
}
} else {
text = event->plainBody();
}
TextHandler textHandler;
textHandler.setData(text);
QList<QUrl> links = textHandler.getLinkPreviews();
if (links.size() > 0) {
return QSharedPointer<LinkPreviewer>(new LinkPreviewer(nullptr, m_room, links.size() > 0 ? links[0] : QUrl()));
} else {
return nullptr;
}
}
bool EventHandler::hasReply() const
{
if (m_event == nullptr) {

View File

@@ -231,16 +231,6 @@ public:
*/
QVariantMap getMediaInfo() const;
/**
* @brief Return a LinkPreviewer object for the event.
*
* A nullptr will be returned for any event that doesn't have any links so the
* return should be null checked and an empty LinkPreviewer provided if null.
*
* @sa LinkPreviewer
*/
QSharedPointer<LinkPreviewer> getLinkPreviewer() const;
/**
* @brief Whether the event is a reply to another in the timeline.
*/

View File

@@ -3,25 +3,37 @@
#include "linkpreviewer.h"
#include "controller.h"
#include <Quotient/connection.h>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/events/roommessageevent.h>
#include "neochatconfig.h"
#include "neochatroom.h"
#include "utils.h"
using namespace Quotient;
LinkPreviewer::LinkPreviewer(QObject *parent, const NeoChatRoom *room, const QUrl &url)
: QObject(parent)
LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event)
: QObject(nullptr)
, m_currentRoom(room)
, m_event(event)
, m_loaded(false)
, m_url(url)
, m_url(linkPreview(event))
{
loadUrlPreview();
if (m_currentRoom) {
connect(this, &LinkPreviewer::urlChanged, this, &LinkPreviewer::emptyChanged);
if (m_event != nullptr && m_currentRoom != nullptr) {
loadUrlPreview();
connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview);
// Make sure that we react to edits
connect(m_currentRoom, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
if (m_event->id() == newEvent->id()) {
m_event = eventCast<const Quotient::RoomMessageEvent>(newEvent);
m_url = linkPreview(m_event);
Q_EMIT urlChanged();
loadUrlPreview();
}
});
}
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview);
}
@@ -51,15 +63,6 @@ QUrl LinkPreviewer::url() const
return m_url;
}
void LinkPreviewer::setUrl(QUrl url)
{
if (url != m_url) {
m_url = url;
urlChanged();
loadUrlPreview();
}
}
void LinkPreviewer::loadUrlPreview()
{
if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) {
@@ -98,4 +101,38 @@ bool LinkPreviewer::empty() const
return m_url.isEmpty();
}
QUrl LinkPreviewer::linkPreview(const Quotient::RoomMessageEvent *event)
{
if (event == nullptr) {
return {};
}
QString text;
if (event->hasTextContent()) {
auto textContent = static_cast<const Quotient::EventContent::TextContent *>(event->content());
if (textContent) {
text = textContent->body;
} else {
text = event->plainBody();
}
} else {
text = event->plainBody();
}
auto data = text.remove(TextRegex::removeRichReply);
auto linksMatch = TextRegex::url.globalMatch(data);
while (linksMatch.hasNext()) {
auto link = linksMatch.next().captured();
if (!link.contains(QStringLiteral("matrix.to"))) {
return QUrl(link);
}
}
return {};
}
bool LinkPreviewer::hasPreviewableLinks(const Quotient::RoomMessageEvent *event)
{
return !linkPreview(event).isEmpty();
}
#include "moc_linkpreviewer.cpp"

View File

@@ -7,6 +7,11 @@
#include <QQmlEngine>
#include <QUrl>
namespace Quotient
{
class RoomMessageEvent;
}
class NeoChatRoom;
/**
@@ -25,7 +30,7 @@ class LinkPreviewer : public QObject
/**
* @brief The URL to get the preview for.
*/
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
Q_PROPERTY(QUrl url READ url NOTIFY urlChanged)
/**
* @brief Whether the preview information has been loaded.
@@ -55,18 +60,25 @@ class LinkPreviewer : public QObject
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
public:
explicit LinkPreviewer(QObject *parent = nullptr, const NeoChatRoom *room = nullptr, const QUrl &url = {});
explicit LinkPreviewer(const NeoChatRoom *room = nullptr, const Quotient::RoomMessageEvent *event = nullptr);
[[nodiscard]] QUrl url() const;
void setUrl(QUrl);
[[nodiscard]] bool loaded() const;
[[nodiscard]] QString title() const;
[[nodiscard]] QString description() const;
[[nodiscard]] QUrl imageSource() const;
[[nodiscard]] bool empty() const;
/**
* @brief Whether the given event has at least 1 pre-viewable link.
*
* A link is only pre-viewable if it is http, https or something starting with www.
*/
static bool hasPreviewableLinks(const Quotient::RoomMessageEvent *event);
private:
const NeoChatRoom *m_currentRoom = nullptr;
const NeoChatRoom *m_currentRoom;
const Quotient::RoomMessageEvent *m_event;
bool m_loaded;
QString m_title = QString();
@@ -76,6 +88,14 @@ private:
void loadUrlPreview();
/**
* @brief Return the link to be previewed from the given event.
*
* This function is designed to give only links that should be previewed so
* http, https or something starting with www. The first valid link is returned.
*/
static QUrl linkPreview(const Quotient::RoomMessageEvent *event);
Q_SIGNALS:
void loadedChanged();
void titleChanged();

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
#include "messageeventmodel.h"
#include "linkpreviewer.h"
#include "messageeventmodel_logging.h"
#include "neochatconfig.h"
@@ -22,6 +23,7 @@
#include "eventhandler.h"
#include "events/pollevent.h"
#include "models/reactionmodel.h"
#include "texthandler.h"
using namespace Quotient;
@@ -237,6 +239,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
});
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
if (message != nullptr) {
createEventObjects(message);
}
});
connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) {
if (eventId.isEmpty()) { // How did we get here?
@@ -720,14 +726,14 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
{
auto eventId = event->id();
EventHandler eventHandler;
eventHandler.setRoom(m_currentRoom);
eventHandler.setEvent(event);
if (auto linkPreviewer = eventHandler.getLinkPreviewer()) {
m_linkPreviewers[eventId] = linkPreviewer;
if (m_linkPreviewers.contains(eventId)) {
if (!LinkPreviewer::hasPreviewableLinks(event)) {
m_linkPreviewers.remove(eventId);
}
} else {
m_linkPreviewers.remove(eventId);
if (LinkPreviewer::hasPreviewableLinks(event)) {
m_linkPreviewers[eventId] = QSharedPointer<LinkPreviewer>(new LinkPreviewer(m_currentRoom, event));
}
}
// ReactionModel handles updates to add and remove reactions, we only need to

View File

@@ -493,18 +493,4 @@ QString TextHandler::linkifyUrls(QString stringIn)
return stringIn;
}
QList<QUrl> TextHandler::getLinkPreviews()
{
auto data = m_data.remove(TextRegex::removeRichReply);
auto linksMatch = TextRegex::url.globalMatch(data);
QList<QUrl> links;
while (linksMatch.hasNext()) {
auto link = linksMatch.next().captured();
if (!link.contains(QStringLiteral("matrix.to"))) {
links += QUrl(link);
}
}
return links;
}
#include "moc_texthandler.cpp"

View File

@@ -11,29 +11,9 @@
#include "neochatroom.h"
namespace TextRegex
namespace Quotient
{
static const QRegularExpression endTagType{QStringLiteral("(>| )")};
static const QRegularExpression attributeData{QStringLiteral("['\"](.*?)['\"]")};
static const QRegularExpression removeReply{QStringLiteral("> <.*?>.*?\\n\\n"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression removeRichReply{QStringLiteral("<mx-reply>.*?</mx-reply>"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression codePill{QStringLiteral("<pre><code[^>]*>(.*?)</code></pre>"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression userPill{QStringLiteral("(<a href=\"https://matrix.to/#/@.*?:.*?\">.*?</a>)"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression blockQuote{QStringLiteral("<blockquote>\n?(?:<p>)?(.*?)(?:</p>)?\n?</blockquote>"),
QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression strikethrough{QStringLiteral("<del>(.*?)</del>"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression mxcImage{QStringLiteral(R"AAA(<img(.*?)src="mxc:\/\/(.*?)\/(.*?)"(.*?)>)AAA")};
static const QRegularExpression plainUrl(
QStringLiteral(
R"(<a.*?<\/a>(*SKIP)(*F)|\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp):(//)?\w|(magnet|matrix):)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
static const QRegularExpression
url(QStringLiteral(R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|https?:(//)?\w)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
static const QRegularExpression emailAddress(QStringLiteral(R"(<a.*?<\/a>(*SKIP)(*F)|\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))"),
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
static const QRegularExpression mxId(QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"),
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
class RoomMessageEvent;
}
/**
@@ -114,14 +94,6 @@ public:
*/
QString handleRecievePlainText(Qt::TextFormat inputFormat = Qt::PlainText, const bool &stripNewlines = false);
/**
* @brief Return a list of links that can be previewed.
*
* This function is designed to give only links that should be previewed so
* http, https or something starting with www.
*/
QList<QUrl> getLinkPreviews();
private:
QString m_data;

View File

@@ -4,6 +4,7 @@
#include <QColor>
#include <QGuiApplication>
#include <QPalette>
#include <QRegularExpression>
namespace Utils
{
@@ -21,3 +22,28 @@ inline QColor getUserColor(qreal hueF)
return QColor::fromHslF(hueF, 1, -0.7 * lightness + 0.9, 1);
}
}
namespace TextRegex
{
static const QRegularExpression endTagType{QStringLiteral("(>| )")};
static const QRegularExpression attributeData{QStringLiteral("['\"](.*?)['\"]")};
static const QRegularExpression removeReply{QStringLiteral("> <.*?>.*?\\n\\n"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression removeRichReply{QStringLiteral("<mx-reply>.*?</mx-reply>"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression codePill{QStringLiteral("<pre><code[^>]*>(.*?)</code></pre>"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression userPill{QStringLiteral("(<a href=\"https://matrix.to/#/@.*?:.*?\">.*?</a>)"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression blockQuote{QStringLiteral("<blockquote>\n?(?:<p>)?(.*?)(?:</p>)?\n?</blockquote>"),
QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression strikethrough{QStringLiteral("<del>(.*?)</del>"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression mxcImage{QStringLiteral(R"AAA(<img(.*?)src="mxc:\/\/(.*?)\/(.*?)"(.*?)>)AAA")};
static const QRegularExpression plainUrl(
QStringLiteral(
R"(<a.*?<\/a>(*SKIP)(*F)|\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp):(//)?\w|(magnet|matrix):)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
static const QRegularExpression
url(QStringLiteral(R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|https?:(//)?\w)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
static const QRegularExpression emailAddress(QStringLiteral(R"(<a.*?<\/a>(*SKIP)(*F)|\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))"),
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
static const QRegularExpression mxId(QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"),
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
}