Link preview messageeventmodel parameters
This move the finding of links and the creation of a `linkpreviewer` into c++. - The links are now extracted from the text in `texthandler` - The `messageeventmodel` now creates and stores `linkpreviewers` for events that have links in the current room. Two new model roles have been created to let a text delegate know when the link preview should be shown (`showLinkPreview`) and pass the link previewer (`linkPreviewer`). Empty link previewer are returned where link don't exist so the qml doesn't have to have checks for whether the parameters are undefined.
This commit is contained in:
committed by
Tobias Fella
parent
b82d3ab5ad
commit
72de7c6cfb
@@ -64,6 +64,11 @@ private Q_SLOTS:
|
||||
void receiveRichEdited_data();
|
||||
void receiveRichEdited();
|
||||
void receiveLineSeparator();
|
||||
|
||||
void linkPreviewsMatch_data();
|
||||
void linkPreviewsMatch();
|
||||
void linkPreviewsReject_data();
|
||||
void linkPreviewsReject();
|
||||
};
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
@@ -596,5 +601,52 @@ void TextHandlerTest::receiveLineSeparator()
|
||||
QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), QStringLiteral("foo bar"));
|
||||
}
|
||||
|
||||
void TextHandlerTest::linkPreviewsMatch_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QList<QUrl>>("testOutputLinks");
|
||||
|
||||
QTest::newRow("plainHttps") << QStringLiteral("https://kde.org") << QList<QUrl>({QUrl("https://kde.org")});
|
||||
QTest::newRow("richHttps") << QStringLiteral("<a href=\"https://kde.org\">Rich Link</a>") << QList<QUrl>({QUrl("https://kde.org")});
|
||||
QTest::newRow("plainWww") << QStringLiteral("www.example.org") << QList<QUrl>({QUrl("www.example.org")});
|
||||
QTest::newRow("multipleHttps") << QStringLiteral("https://kde.org www.example.org")
|
||||
<< QList<QUrl>({
|
||||
QUrl("https://kde.org"),
|
||||
QUrl("www.example.org"),
|
||||
});
|
||||
}
|
||||
|
||||
void TextHandlerTest::linkPreviewsMatch()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QList<QUrl>, testOutputLinks);
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
|
||||
}
|
||||
|
||||
void TextHandlerTest::linkPreviewsReject_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QList<QUrl>>("testOutputLinks");
|
||||
|
||||
QTest::newRow("mxc") << QStringLiteral("mxc://example.org/SEsfnsuifSDFSSEF") << QList<QUrl>();
|
||||
QTest::newRow("matrixTo") << QStringLiteral("https://matrix.to/#/@alice:example.org") << QList<QUrl>();
|
||||
QTest::newRow("noSpace") << QStringLiteral("testhttps://kde.org") << QList<QUrl>();
|
||||
}
|
||||
|
||||
void TextHandlerTest::linkPreviewsReject()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QList<QUrl>, testOutputLinks);
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TextHandlerTest)
|
||||
#include "texthandlertest.moc"
|
||||
|
||||
@@ -10,24 +10,13 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
LinkPreviewer::LinkPreviewer(QObject *parent)
|
||||
LinkPreviewer::LinkPreviewer(QObject *parent, NeoChatRoom *room, QUrl url)
|
||||
: QObject(parent)
|
||||
, m_currentRoom(room)
|
||||
, m_loaded(false)
|
||||
, m_url(url)
|
||||
{
|
||||
}
|
||||
|
||||
NeoChatRoom *LinkPreviewer::room() const
|
||||
{
|
||||
return m_currentRoom;
|
||||
}
|
||||
|
||||
void LinkPreviewer::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
if (room == m_currentRoom) {
|
||||
return;
|
||||
}
|
||||
m_currentRoom = room;
|
||||
Q_EMIT roomChanged();
|
||||
loadUrlPreview();
|
||||
}
|
||||
|
||||
bool LinkPreviewer::loaded() const
|
||||
@@ -57,13 +46,19 @@ QUrl LinkPreviewer::url() const
|
||||
|
||||
void LinkPreviewer::setUrl(QUrl url)
|
||||
{
|
||||
if (url.scheme() == QStringLiteral("https")) {
|
||||
if (url != m_url) {
|
||||
m_url = url;
|
||||
urlChanged();
|
||||
loadUrlPreview();
|
||||
}
|
||||
}
|
||||
|
||||
void LinkPreviewer::loadUrlPreview()
|
||||
{
|
||||
if (m_url.scheme() == QStringLiteral("https")) {
|
||||
m_loaded = false;
|
||||
Q_EMIT loadedChanged();
|
||||
|
||||
m_url = url;
|
||||
Q_EMIT urlChanged();
|
||||
|
||||
auto conn = m_currentRoom->connection();
|
||||
GetUrlPreviewJob *job = conn->callApi<GetUrlPreviewJob>(m_url.toString());
|
||||
|
||||
|
||||
@@ -19,12 +19,6 @@ class NeoChatRoom;
|
||||
class LinkPreviewer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The current room that the URL is from.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief The URL to get the preview for.
|
||||
*/
|
||||
@@ -51,10 +45,7 @@ class LinkPreviewer : public QObject
|
||||
Q_PROPERTY(QUrl imageSource READ imageSource NOTIFY imageSourceChanged)
|
||||
|
||||
public:
|
||||
explicit LinkPreviewer(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
explicit LinkPreviewer(QObject *parent = nullptr, NeoChatRoom *room = nullptr, QUrl url = {});
|
||||
|
||||
[[nodiscard]] QUrl url() const;
|
||||
void setUrl(QUrl);
|
||||
@@ -67,16 +58,18 @@ private:
|
||||
NeoChatRoom *m_currentRoom = nullptr;
|
||||
|
||||
bool m_loaded;
|
||||
QString m_title;
|
||||
QString m_description;
|
||||
QUrl m_imageSource;
|
||||
QString m_title = QString();
|
||||
QString m_description = QString();
|
||||
QUrl m_imageSource = QUrl();
|
||||
QUrl m_url;
|
||||
|
||||
void loadUrlPreview();
|
||||
|
||||
Q_SIGNALS:
|
||||
void roomChanged();
|
||||
void loadedChanged();
|
||||
void titleChanged();
|
||||
void descriptionChanged();
|
||||
void imageSourceChanged();
|
||||
void urlChanged();
|
||||
};
|
||||
Q_DECLARE_METATYPE(LinkPreviewer *)
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "neochatuser.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -45,6 +46,8 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[SpecialMarksRole] = "marks";
|
||||
roles[LongOperationRole] = "progressInfo";
|
||||
roles[EventResolvedTypeRole] = "eventResolvedType";
|
||||
roles[ShowLinkPreviewRole] = "showLinkPreview";
|
||||
roles[LinkPreviewRole] = "linkPreview";
|
||||
roles[MediaInfoRole] = "mediaInfo";
|
||||
roles[IsReplyRole] = "isReply";
|
||||
roles[ReplyAuthor] = "replyAuthor";
|
||||
@@ -101,12 +104,20 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
beginResetModel();
|
||||
if (m_currentRoom) {
|
||||
m_currentRoom->disconnect(this);
|
||||
m_linkPreviewers.clear();
|
||||
}
|
||||
|
||||
m_currentRoom = room;
|
||||
if (room) {
|
||||
m_lastReadEventIndex = QPersistentModelIndex(QModelIndex());
|
||||
room->setDisplayed();
|
||||
|
||||
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
|
||||
if (auto e = &*event->viewAs<RoomMessageEvent>()) {
|
||||
createLinkPreviewerForEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
|
||||
room->getPreviousContent(50);
|
||||
}
|
||||
@@ -118,10 +129,12 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
|
||||
using namespace Quotient;
|
||||
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
||||
if (NeoChatConfig::self()->showFancyEffects()) {
|
||||
for (auto &event : events) {
|
||||
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||
if (message) {
|
||||
for (auto &&event : events) {
|
||||
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||
if (message != nullptr) {
|
||||
createLinkPreviewerForEvent(message);
|
||||
|
||||
if (NeoChatConfig::self()->showFancyEffects()) {
|
||||
QString planBody = message->plainBody();
|
||||
// snowflake
|
||||
const QString snowlakeEmoji = QString::fromUtf8("\xE2\x9D\x84");
|
||||
@@ -155,6 +168,12 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1);
|
||||
});
|
||||
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
|
||||
for (auto &event : events) {
|
||||
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||
if (message) {
|
||||
createLinkPreviewerForEvent(message);
|
||||
}
|
||||
}
|
||||
if (rowCount() > 0) {
|
||||
rowBelowInserted = rowCount() - 1; // See #312
|
||||
}
|
||||
@@ -455,6 +474,8 @@ inline QVariantMap userInContext(NeoChatUser *user, NeoChatRoom *room)
|
||||
};
|
||||
}
|
||||
|
||||
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
|
||||
|
||||
QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
{
|
||||
const auto row = idx.row();
|
||||
@@ -684,6 +705,18 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return role == TimeRole ? QVariant(ts) : renderDate(ts);
|
||||
}
|
||||
|
||||
if (role == ShowLinkPreviewRole) {
|
||||
return m_linkPreviewers.contains(evt.id());
|
||||
}
|
||||
|
||||
if (role == LinkPreviewRole) {
|
||||
if (m_linkPreviewers.contains(evt.id())) {
|
||||
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()]);
|
||||
} else {
|
||||
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
|
||||
}
|
||||
}
|
||||
|
||||
if (role == MediaInfoRole) {
|
||||
return getMediaInfoForEvent(evt);
|
||||
}
|
||||
@@ -1197,3 +1230,29 @@ QVariantMap MessageEventModel::getMediaInfoFromFileInfo(const EventContent::File
|
||||
|
||||
return mediaInfo;
|
||||
}
|
||||
|
||||
void MessageEventModel::createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event)
|
||||
{
|
||||
if (m_linkPreviewers.contains(event->id())) {
|
||||
return;
|
||||
} else {
|
||||
QString text;
|
||||
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) {
|
||||
m_linkPreviewers[event->id()] = new LinkPreviewer(nullptr, m_currentRoom, links.size() > 0 ? links[0] : QUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include "linkpreviewer.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
/**
|
||||
@@ -70,6 +71,9 @@ public:
|
||||
FormattedBodyRole, /**< The formatted body of a rich message. */
|
||||
GenericDisplayRole, /**< A generic string based upon the message type. */
|
||||
|
||||
ShowLinkPreviewRole, /**< Whether a link preview should be shown. */
|
||||
LinkPreviewRole, /**< The link preview details. */
|
||||
|
||||
MediaInfoRole, /**< The media info for the event. */
|
||||
MimeTypeRole, /**< The mime type of the message's file or media. */
|
||||
|
||||
@@ -180,6 +184,8 @@ private:
|
||||
int rowBelowInserted = -1;
|
||||
bool movingEvent = false;
|
||||
|
||||
QMap<QString, LinkPreviewer *> m_linkPreviewers;
|
||||
|
||||
[[nodiscard]] int timelineBaseIndex() const;
|
||||
[[nodiscard]] QDateTime makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const;
|
||||
[[nodiscard]] static QString renderDate(const QDateTime ×tamp);
|
||||
@@ -195,6 +201,7 @@ private:
|
||||
const Quotient::RoomEvent *getReplyForEvent(const Quotient::RoomEvent &event) const;
|
||||
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent &event) const;
|
||||
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
|
||||
void createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event);
|
||||
|
||||
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_extraEvents;
|
||||
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
|
||||
|
||||
@@ -14,33 +14,16 @@ Loader {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The room that the component is created in.
|
||||
*/
|
||||
property var room
|
||||
|
||||
/**
|
||||
* @brief Get a list of hyperlinks in the text.
|
||||
* @brief The link preview properties.
|
||||
*
|
||||
* User links i.e. anything starting with https://matrix.to are ignored.
|
||||
* This is a list or object containing the following:
|
||||
* - url - The URL being previewed.
|
||||
* - loaded - Whether the URL preview has been loaded.
|
||||
* - title - the title of the URL preview.
|
||||
* - description - the description of the URL preview.
|
||||
* - imageSource - a source URL for the preview image.
|
||||
*/
|
||||
property var links: {
|
||||
let matches = model.display.match(/\bhttps?:\/\/[^\s\<\>\"\']+/g)
|
||||
if (matches && matches.length > 0) {
|
||||
// don't show previews for room links or user mentions or custom emojis
|
||||
return matches.filter(link => !(
|
||||
link.includes("https://matrix.to") || link.includes("/_matrix/media/r0/download/")
|
||||
))
|
||||
// remove ending fullstops and commas
|
||||
.map(link => (link.length && [".", ","].includes(link[link.length-1])) ? link.substring(0, link.length-1) : link)
|
||||
}
|
||||
return []
|
||||
|
||||
}
|
||||
LinkPreviewer {
|
||||
id: linkPreviewer
|
||||
room: root.room
|
||||
url: root.links && root.links.length > 0 ? root.links[0] : ""
|
||||
}
|
||||
property var linkPreviewer
|
||||
|
||||
/**
|
||||
* @brief Standard height for the link preview.
|
||||
@@ -55,8 +38,7 @@ Loader {
|
||||
*/
|
||||
property bool indicatorEnabled: false
|
||||
|
||||
active: !currentRoom.usesEncryption && model.display && links && links.length > 0 && currentRoom.urlPreviewEnabled
|
||||
visible: Config.showLinkPreview && active
|
||||
visible: active
|
||||
sourceComponent: linkPreviewer.loaded ? linkPreviewComponent : loadingComponent
|
||||
|
||||
Component {
|
||||
@@ -95,16 +77,16 @@ Loader {
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
text: "<style>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<a href=\"" + root.links[0] + "\">" + (maximizeButton.checked ? linkPreviewer.title : titleTextMetrics.elidedText).replace("–", "—") + "</a>"
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<a href=\"" + root.linkPreviewer.url + "\">" + (maximizeButton.checked ? root.linkPreviewer.title : titleTextMetrics.elidedText).replace("–", "—") + "</a>"
|
||||
onLinkActivated: RoomManager.openResource(link)
|
||||
|
||||
TextMetrics {
|
||||
id: titleTextMetrics
|
||||
text: linkPreviewer.title
|
||||
text: root.linkPreviewer.title
|
||||
font: linkPreviewTitle.font
|
||||
elide: Text.ElideRight
|
||||
elideWidth: (linkPreviewTitle.width - Kirigami.Units.largeSpacing * 2.5) * 3
|
||||
|
||||
@@ -35,7 +35,8 @@ TimelineContainer {
|
||||
}
|
||||
LinkPreviewDelegate {
|
||||
Layout.fillWidth: true
|
||||
room: currentRoom
|
||||
active: !currentRoom.usesEncryption && currentRoom.urlPreviewEnabled && Config.showLinkPreview && model.showLinkPreview
|
||||
linkPreviewer: model.linkPreview
|
||||
indicatorEnabled: messageDelegate.isVisibleInTimeline()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +430,21 @@ QString TextHandler::unescapeHtml(QString stringIn)
|
||||
QString TextHandler::linkifyUrls(QString stringIn)
|
||||
{
|
||||
stringIn = stringIn.replace(TextRegex::mxId, QStringLiteral(R"(\1<a href="https://matrix.to/#/\2">\2</a>)"));
|
||||
stringIn.replace(TextRegex::fullUrl, QStringLiteral(R"(<a href="\1">\1</a>)"));
|
||||
stringIn.replace(TextRegex::plainUrl, QStringLiteral(R"(<a href="\1">\1</a>)"));
|
||||
stringIn = stringIn.replace(TextRegex::emailAddress, QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)"));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -21,10 +21,13 @@ static const QRegularExpression codePill{QStringLiteral("<pre><code[^>]*>(.*?)</
|
||||
static const QRegularExpression userPill{QStringLiteral("(<a href=\"https://matrix.to/#/@.*?:.*?\">.*?</a>)"), 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 fullUrl(
|
||||
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})?))"),
|
||||
@@ -109,6 +112,14 @@ 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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user