Multiple Link Previews
- Show a preview for each link in the text below the block in which it appears. - Allow link previews to be dismissed
This commit is contained in:
@@ -89,38 +89,23 @@ bool LinkPreviewer::empty() const
|
||||
return m_url.isEmpty();
|
||||
}
|
||||
|
||||
QUrl LinkPreviewer::linkPreview(const Quotient::RoomMessageEvent *event)
|
||||
QList<QUrl> LinkPreviewer::linkPreviews(QString string)
|
||||
{
|
||||
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 data = string.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"))) {
|
||||
return QUrl(link);
|
||||
if (!link.contains(QStringLiteral("matrix.to")) && !links.contains(QUrl(link))) {
|
||||
links += QUrl(link);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
return links;
|
||||
}
|
||||
|
||||
bool LinkPreviewer::hasPreviewableLinks(const Quotient::RoomMessageEvent *event)
|
||||
bool LinkPreviewer::hasPreviewableLinks(const QString &string)
|
||||
{
|
||||
return !linkPreview(event).isEmpty();
|
||||
return !linkPreviews(string).isEmpty();
|
||||
}
|
||||
|
||||
#include "moc_linkpreviewer.cpp"
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
#include <QQmlEngine>
|
||||
#include <QUrl>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class RoomMessageEvent;
|
||||
}
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
@@ -71,19 +66,19 @@ public:
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the given event has at least 1 pre-viewable link.
|
||||
* @brief Whether the given string 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);
|
||||
static bool hasPreviewableLinks(const QString &string);
|
||||
|
||||
/**
|
||||
* @brief Return the link to be previewed from the given event.
|
||||
* @brief Return previewable links from the given string.
|
||||
*
|
||||
* 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);
|
||||
static QList<QUrl> linkPreviews(QString string);
|
||||
|
||||
private:
|
||||
bool m_loaded;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <qlist.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <KSyntaxHighlighting/Definition>
|
||||
@@ -110,11 +111,14 @@ MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoCh
|
||||
endResetModel();
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, &MessageContentModel::updateLinkPreviewer);
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &MessageContentModel::updateLinkPreviewer);
|
||||
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
|
||||
updateComponents();
|
||||
});
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, [this]() {
|
||||
updateComponents();
|
||||
});
|
||||
}
|
||||
|
||||
updateLinkPreviewer();
|
||||
updateComponents();
|
||||
}
|
||||
|
||||
@@ -197,8 +201,9 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
return eventHandler.getReplyMediaInfo();
|
||||
}
|
||||
if (role == LinkPreviewerRole) {
|
||||
if (m_linkPreviewer != nullptr) {
|
||||
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewer);
|
||||
if (component.type == MessageComponentType::LinkPreview) {
|
||||
return QVariant::fromValue<LinkPreviewer *>(
|
||||
dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(component.attributes["link"_ls].toUrl()));
|
||||
} else {
|
||||
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
|
||||
}
|
||||
@@ -272,13 +277,7 @@ void MessageContentModel::updateComponents(bool isEditing)
|
||||
m_components.append(componentsForType(eventHandler.messageComponentType()));
|
||||
}
|
||||
|
||||
if (m_linkPreviewer != nullptr) {
|
||||
if (m_linkPreviewer->loaded()) {
|
||||
m_components += MessageComponent{MessageComponentType::LinkPreview, QString(), {}};
|
||||
} else {
|
||||
m_components += MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {}};
|
||||
}
|
||||
}
|
||||
addLinkPreviews();
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
@@ -326,41 +325,60 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
|
||||
}
|
||||
}
|
||||
|
||||
void MessageContentModel::updateLinkPreviewer()
|
||||
MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
|
||||
{
|
||||
if (m_room == nullptr || m_event == nullptr) {
|
||||
if (m_linkPreviewer != nullptr) {
|
||||
m_linkPreviewer->disconnect(this);
|
||||
m_linkPreviewer = nullptr;
|
||||
updateComponents();
|
||||
}
|
||||
return;
|
||||
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
|
||||
if (linkPreviewer == nullptr) {
|
||||
return {};
|
||||
}
|
||||
if (!m_room->urlPreviewEnabled()) {
|
||||
if (m_linkPreviewer != nullptr) {
|
||||
m_linkPreviewer->disconnect(this);
|
||||
m_linkPreviewer = nullptr;
|
||||
updateComponents();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
|
||||
if (LinkPreviewer::hasPreviewableLinks(event)) {
|
||||
m_linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(LinkPreviewer::linkPreview(event));
|
||||
updateComponents();
|
||||
|
||||
if (m_linkPreviewer != nullptr) {
|
||||
connect(m_linkPreviewer, &LinkPreviewer::loadedChanged, [this]() {
|
||||
if (m_linkPreviewer != nullptr && m_linkPreviewer->loaded()) {
|
||||
if (linkPreviewer->loaded()) {
|
||||
return MessageComponent{MessageComponentType::LinkPreview, QString(), {{"link"_ls, link}}};
|
||||
} else {
|
||||
connect(linkPreviewer, &LinkPreviewer::loadedChanged, [this, link]() {
|
||||
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
|
||||
if (linkPreviewer != nullptr && linkPreviewer->loaded()) {
|
||||
for (auto &component : m_components) {
|
||||
if (component.attributes["link"_ls].toUrl() == link) {
|
||||
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
|
||||
beginResetModel();
|
||||
m_components[m_components.size() - 1].type = MessageComponentType::LinkPreview;
|
||||
component.type = MessageComponentType::LinkPreview;
|
||||
endResetModel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_ls, link}}};
|
||||
}
|
||||
}
|
||||
|
||||
void MessageContentModel::addLinkPreviews()
|
||||
{
|
||||
int i = 0;
|
||||
while (i < m_components.size()) {
|
||||
const auto component = m_components.at(i);
|
||||
if (component.type == MessageComponentType::Text || component.type == MessageComponentType::Quote) {
|
||||
if (LinkPreviewer::hasPreviewableLinks(component.content)) {
|
||||
const auto links = LinkPreviewer::linkPreviews(component.content);
|
||||
for (qsizetype j = 0; j < links.size(); ++j) {
|
||||
if (!m_removedLinkPreviews.contains(links[j])) {
|
||||
m_components.insert(i + j + 1, linkPreviewComponent(links[j]));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
void MessageContentModel::closeLinkPreview(int row)
|
||||
{
|
||||
if (m_components[row].type == MessageComponentType::LinkPreview || m_components[row].type == MessageComponentType::LinkPreviewLoad) {
|
||||
beginResetModel();
|
||||
m_removedLinkPreviews += m_components[row].attributes["link"_ls].toUrl();
|
||||
m_components.remove(row);
|
||||
m_components.squeeze();
|
||||
updateComponents();
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,13 @@ public:
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Close the link preview at the given index.
|
||||
*
|
||||
* If the given index is not a link preview component, nothing happens.
|
||||
*/
|
||||
Q_INVOKABLE void closeLinkPreview(int row);
|
||||
|
||||
private:
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
const Quotient::RoomEvent *m_event = nullptr;
|
||||
@@ -95,12 +102,14 @@ private:
|
||||
QList<MessageComponent> m_components;
|
||||
void updateComponents(bool isEditing = false);
|
||||
|
||||
QPointer<LinkPreviewer> m_linkPreviewer;
|
||||
ItineraryModel *m_itineraryModel = nullptr;
|
||||
|
||||
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
|
||||
MessageComponent linkPreviewComponent(const QUrl &link);
|
||||
void addLinkPreviews();
|
||||
|
||||
QList<QUrl> m_removedLinkPreviews;
|
||||
|
||||
void updateLinkPreviewer();
|
||||
void updateItineraryModel();
|
||||
bool m_emptyItinerary = false;
|
||||
|
||||
|
||||
@@ -166,6 +166,7 @@ QQC2.Control {
|
||||
root.selectedTextChanged(selectedText);
|
||||
}
|
||||
onShowMessageMenu: root.showMessageMenu()
|
||||
onRemoveLinkPreview: index => root.contentModel.closeLinkPreview(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ import org.kde.kirigami as Kirigami
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The index of the delegate in the model.
|
||||
*/
|
||||
required property int index
|
||||
|
||||
/**
|
||||
* @brief The link preview properties.
|
||||
*
|
||||
@@ -32,7 +37,7 @@ QQC2.Control {
|
||||
* When the content of the link preview is larger than this it will be
|
||||
* elided/hidden until maximized.
|
||||
*/
|
||||
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2
|
||||
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.largeSpacing * 2
|
||||
|
||||
property bool truncated: linkPreviewDescription.truncated || !linkPreviewDescription.visible
|
||||
|
||||
@@ -41,6 +46,11 @@ QQC2.Control {
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
/**
|
||||
* @brief Request for this delegate to be removed.
|
||||
*/
|
||||
signal remove(int index)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
@@ -110,6 +120,23 @@ QQC2.Control {
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: closeButton
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
visible: root.hovered
|
||||
text: i18nc("As in remove the link preview so it's no longer shown", "Remove preview")
|
||||
icon.name: "dialog-close"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: root.remove(root.index)
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: closeButton.text
|
||||
visible: closeButton.hovered
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
id: maximizeButton
|
||||
anchors.right: parent.right
|
||||
@@ -122,7 +149,7 @@ QQC2.Control {
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: maximizeButton.text
|
||||
visible: hovered
|
||||
visible: maximizeButton.hovered
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,14 @@ import org.kde.kirigami as Kirigami
|
||||
/**
|
||||
* @brief A component to show a link preview loading from a message.
|
||||
*/
|
||||
RowLayout {
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The index of the delegate in the model.
|
||||
*/
|
||||
required property int index
|
||||
|
||||
required property int type
|
||||
|
||||
/**
|
||||
@@ -28,6 +33,11 @@ RowLayout {
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
/**
|
||||
* @brief Request for this delegate to be removed.
|
||||
*/
|
||||
signal remove(int index)
|
||||
|
||||
enum Type {
|
||||
Reply,
|
||||
LinkPreview
|
||||
@@ -36,24 +46,46 @@ RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
width: Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
QQC2.BusyIndicator {}
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: root.defaultHeight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
level: 2
|
||||
text: {
|
||||
switch (root.type) {
|
||||
case LoadComponent.Reply:
|
||||
return i18n("Loading reply");
|
||||
case LoadComponent.LinkPreview:
|
||||
return i18n("Loading URL preview");
|
||||
contentItem : RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
width: Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
QQC2.BusyIndicator {}
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: root.defaultHeight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
level: 2
|
||||
text: {
|
||||
switch (root.type) {
|
||||
case LoadComponent.Reply:
|
||||
return i18n("Loading reply");
|
||||
case LoadComponent.LinkPreview:
|
||||
return i18n("Loading URL preview");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: closeButton
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
visible: root.hovered && root.type === LoadComponent.LinkPreview
|
||||
text: i18nc("As in remove the link preview so it's no longer shown", "Remove preview")
|
||||
icon.name: "dialog-close"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: root.remove(root.index)
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: closeButton.text
|
||||
visible: closeButton.hovered
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ DelegateChooser {
|
||||
*/
|
||||
signal showMessageMenu
|
||||
|
||||
signal removeLinkPreview(int index)
|
||||
|
||||
role: "componentType"
|
||||
|
||||
DelegateChoice {
|
||||
@@ -199,6 +201,7 @@ DelegateChooser {
|
||||
roleValue: MessageComponentType.LinkPreview
|
||||
delegate: LinkPreviewComponent {
|
||||
maxContentWidth: root.maxContentWidth
|
||||
onRemove: index => root.removeLinkPreview(index)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +210,7 @@ DelegateChooser {
|
||||
delegate: LoadComponent {
|
||||
type: LoadComponent.LinkPreview
|
||||
maxContentWidth: root.maxContentWidth
|
||||
onRemove: index => root.removeLinkPreview(index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user