Message Content Rework
For now everything should look identical. However this moves to using a model for the content of the message and is intended to lay the foundation for improved message content representation, e.g. splitting up a text message in multiple sections and using different delegates for things like code and quotes.
This commit is contained in:
@@ -12,7 +12,6 @@
|
|||||||
#include <Quotient/quotient_common.h>
|
#include <Quotient/quotient_common.h>
|
||||||
#include <Quotient/syncdata.h>
|
#include <Quotient/syncdata.h>
|
||||||
|
|
||||||
#include "enums/delegatetype.h"
|
|
||||||
#include "linkpreviewer.h"
|
#include "linkpreviewer.h"
|
||||||
#include "models/reactionmodel.h"
|
#include "models/reactionmodel.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
@@ -37,9 +36,6 @@ private Q_SLOTS:
|
|||||||
|
|
||||||
void eventId();
|
void eventId();
|
||||||
void nullEventId();
|
void nullEventId();
|
||||||
void delegateType_data();
|
|
||||||
void delegateType();
|
|
||||||
void nullDelegateType();
|
|
||||||
void author();
|
void author();
|
||||||
void nullAuthor();
|
void nullAuthor();
|
||||||
void authorDisplayName();
|
void authorDisplayName();
|
||||||
@@ -67,8 +63,6 @@ private Q_SLOTS:
|
|||||||
void nullHasReply();
|
void nullHasReply();
|
||||||
void replyId();
|
void replyId();
|
||||||
void nullReplyId();
|
void nullReplyId();
|
||||||
void replyDelegateType();
|
|
||||||
void nullReplyDelegateType();
|
|
||||||
void replyAuthor();
|
void replyAuthor();
|
||||||
void nullReplyAuthor();
|
void nullReplyAuthor();
|
||||||
void replyBody();
|
void replyBody();
|
||||||
@@ -102,35 +96,6 @@ void EventHandlerTest::nullEventId()
|
|||||||
QCOMPARE(noEventHandler.getId(), QString());
|
QCOMPARE(noEventHandler.getId(), QString());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EventHandlerTest::delegateType_data()
|
|
||||||
{
|
|
||||||
QTest::addColumn<int>("eventNum");
|
|
||||||
QTest::addColumn<DelegateType::Type>("delegateType");
|
|
||||||
|
|
||||||
QTest::newRow("message") << 0 << DelegateType::Message;
|
|
||||||
QTest::newRow("state") << 1 << DelegateType::State;
|
|
||||||
QTest::newRow("message 2") << 2 << DelegateType::Message;
|
|
||||||
QTest::newRow("reaction") << 3 << DelegateType::Other;
|
|
||||||
QTest::newRow("video") << 4 << DelegateType::Video;
|
|
||||||
QTest::newRow("location") << 7 << DelegateType::Location;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EventHandlerTest::delegateType()
|
|
||||||
{
|
|
||||||
QFETCH(int, eventNum);
|
|
||||||
QFETCH(DelegateType::Type, delegateType);
|
|
||||||
|
|
||||||
EventHandler eventHandler(room, room->messageEvents().at(eventNum).get());
|
|
||||||
QCOMPARE(eventHandler.getDelegateType(), delegateType);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EventHandlerTest::nullDelegateType()
|
|
||||||
{
|
|
||||||
EventHandler noEventHandler(room, nullptr);
|
|
||||||
QTest::ignoreMessage(QtWarningMsg, "getDelegateType called with m_event set to nullptr.");
|
|
||||||
QCOMPARE(noEventHandler.getDelegateType(), DelegateType::Other);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EventHandlerTest::author()
|
void EventHandlerTest::author()
|
||||||
{
|
{
|
||||||
auto event = room->messageEvents().at(0).get();
|
auto event = room->messageEvents().at(0).get();
|
||||||
@@ -409,25 +374,6 @@ void EventHandlerTest::nullReplyId()
|
|||||||
QCOMPARE(noEventHandler.getReplyId(), QString());
|
QCOMPARE(noEventHandler.getReplyId(), QString());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EventHandlerTest::replyDelegateType()
|
|
||||||
{
|
|
||||||
EventHandler eventHandlerReply(room, room->messageEvents().at(5).get());
|
|
||||||
QCOMPARE(eventHandlerReply.getReplyDelegateType(), DelegateType::Message);
|
|
||||||
|
|
||||||
EventHandler eventHandlerNoReply(room, room->messageEvents().at(0).get());
|
|
||||||
QCOMPARE(eventHandlerNoReply.getReplyDelegateType(), DelegateType::Other);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EventHandlerTest::nullReplyDelegateType()
|
|
||||||
{
|
|
||||||
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_room set to nullptr.");
|
|
||||||
QCOMPARE(emptyHandler.getReplyDelegateType(), DelegateType::Other);
|
|
||||||
|
|
||||||
EventHandler noEventHandler(room, nullptr);
|
|
||||||
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_event set to nullptr.");
|
|
||||||
QCOMPARE(noEventHandler.getReplyDelegateType(), DelegateType::Other);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EventHandlerTest::replyAuthor()
|
void EventHandlerTest::replyAuthor()
|
||||||
{
|
{
|
||||||
auto replyEvent = room->messageEvents().at(0).get();
|
auto replyEvent = room->messageEvents().at(0).get();
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ void MessageEventModelTest::simpleTimeline()
|
|||||||
|
|
||||||
QCOMPARE(model->data(model->index(1)), QStringLiteral("<b>This is an example<br>text message</b>"));
|
QCOMPARE(model->data(model->index(1)), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||||
QCOMPARE(model->data(model->index(1), MessageEventModel::DelegateTypeRole), DelegateType::Message);
|
QCOMPARE(model->data(model->index(1), MessageEventModel::DelegateTypeRole), DelegateType::Message);
|
||||||
QCOMPARE(model->data(model->index(1), MessageEventModel::PlainText), QStringLiteral("This is an example\ntext message"));
|
|
||||||
QCOMPARE(model->data(model->index(1), MessageEventModel::EventIdRole), QStringLiteral("$153456789:example.org"));
|
QCOMPARE(model->data(model->index(1), MessageEventModel::EventIdRole), QStringLiteral("$153456789:example.org"));
|
||||||
|
|
||||||
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");
|
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ add_library(neochat STATIC
|
|||||||
events/locationbeaconevent.h
|
events/locationbeaconevent.h
|
||||||
events/serveraclevent.h
|
events/serveraclevent.h
|
||||||
events/widgetevent.h
|
events/widgetevent.h
|
||||||
|
enums/messagecomponenttype.h
|
||||||
|
models/messagecontentmodel.cpp
|
||||||
|
models/messagecontentmodel.h
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||||
@@ -198,21 +201,12 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
|||||||
qml/TimelineDelegate.qml
|
qml/TimelineDelegate.qml
|
||||||
qml/ReplyComponent.qml
|
qml/ReplyComponent.qml
|
||||||
qml/StateDelegate.qml
|
qml/StateDelegate.qml
|
||||||
qml/RichLabel.qml
|
|
||||||
qml/MessageDelegate.qml
|
qml/MessageDelegate.qml
|
||||||
qml/Bubble.qml
|
qml/Bubble.qml
|
||||||
qml/SectionDelegate.qml
|
qml/SectionDelegate.qml
|
||||||
qml/VideoDelegate.qml
|
|
||||||
qml/ReactionDelegate.qml
|
qml/ReactionDelegate.qml
|
||||||
qml/LinkPreviewDelegate.qml
|
|
||||||
qml/AudioDelegate.qml
|
|
||||||
qml/FileDelegate.qml
|
|
||||||
qml/ImageDelegate.qml
|
|
||||||
qml/EncryptedDelegate.qml
|
|
||||||
qml/EventDelegate.qml
|
qml/EventDelegate.qml
|
||||||
qml/TextDelegate.qml
|
|
||||||
qml/ReadMarkerDelegate.qml
|
qml/ReadMarkerDelegate.qml
|
||||||
qml/PollDelegate.qml
|
|
||||||
qml/MimeComponent.qml
|
qml/MimeComponent.qml
|
||||||
qml/StateComponent.qml
|
qml/StateComponent.qml
|
||||||
qml/MessageEditComponent.qml
|
qml/MessageEditComponent.qml
|
||||||
@@ -276,14 +270,12 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
|||||||
qml/EmojiDelegate.qml
|
qml/EmojiDelegate.qml
|
||||||
qml/EmojiGrid.qml
|
qml/EmojiGrid.qml
|
||||||
qml/RoomSearchPage.qml
|
qml/RoomSearchPage.qml
|
||||||
qml/LocationDelegate.qml
|
|
||||||
qml/LocationChooser.qml
|
qml/LocationChooser.qml
|
||||||
qml/TimelineView.qml
|
qml/TimelineView.qml
|
||||||
qml/InvitationView.qml
|
qml/InvitationView.qml
|
||||||
qml/AvatarTabButton.qml
|
qml/AvatarTabButton.qml
|
||||||
qml/SpaceDrawer.qml
|
qml/SpaceDrawer.qml
|
||||||
qml/OsmLocationPlugin.qml
|
qml/OsmLocationPlugin.qml
|
||||||
qml/LiveLocationDelegate.qml
|
|
||||||
qml/FullScreenMap.qml
|
qml/FullScreenMap.qml
|
||||||
qml/LocationsPage.qml
|
qml/LocationsPage.qml
|
||||||
qml/LocationMapItem.qml
|
qml/LocationMapItem.qml
|
||||||
@@ -310,6 +302,18 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
|||||||
qml/ServerComboBox.qml
|
qml/ServerComboBox.qml
|
||||||
qml/UserSearchPage.qml
|
qml/UserSearchPage.qml
|
||||||
qml/ManualUserDialog.qml
|
qml/ManualUserDialog.qml
|
||||||
|
qml/MessageComponentChooser.qml
|
||||||
|
qml/TextComponent.qml
|
||||||
|
qml/ImageComponent.qml
|
||||||
|
qml/VideoComponent.qml
|
||||||
|
qml/AudioComponent.qml
|
||||||
|
qml/EncryptedComponent.qml
|
||||||
|
qml/FileComponent.qml
|
||||||
|
qml/LocationComponent.qml
|
||||||
|
qml/LiveLocationComponent.qml
|
||||||
|
qml/PollComponent.qml
|
||||||
|
qml/LinkPreviewComponent.qml
|
||||||
|
qml/LoadComponent.qml
|
||||||
RESOURCES
|
RESOURCES
|
||||||
qml/confetti.png
|
qml/confetti.png
|
||||||
qml/glowdot.png
|
qml/glowdot.png
|
||||||
|
|||||||
@@ -43,14 +43,14 @@ void ChatBarCache::setReplyId(const QString &replyId)
|
|||||||
if (m_relationType == Reply && m_relationId == replyId) {
|
if (m_relationType == Reply && m_relationId == replyId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_relationId = replyId;
|
const auto oldEventId = std::exchange(m_relationId, replyId);
|
||||||
if (m_relationId.isEmpty()) {
|
if (m_relationId.isEmpty()) {
|
||||||
m_relationType = None;
|
m_relationType = None;
|
||||||
} else {
|
} else {
|
||||||
m_relationType = Reply;
|
m_relationType = Reply;
|
||||||
}
|
}
|
||||||
m_attachmentPath = QString();
|
m_attachmentPath = QString();
|
||||||
Q_EMIT relationIdChanged();
|
Q_EMIT relationIdChanged(oldEventId, m_relationId);
|
||||||
Q_EMIT attachmentPathChanged();
|
Q_EMIT attachmentPathChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,14 +72,14 @@ void ChatBarCache::setEditId(const QString &editId)
|
|||||||
if (m_relationType == Edit && m_relationId == editId) {
|
if (m_relationType == Edit && m_relationId == editId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_relationId = editId;
|
const auto oldEventId = std::exchange(m_relationId, editId);
|
||||||
if (m_relationId.isEmpty()) {
|
if (m_relationId.isEmpty()) {
|
||||||
m_relationType = None;
|
m_relationType = None;
|
||||||
} else {
|
} else {
|
||||||
m_relationType = Edit;
|
m_relationType = Edit;
|
||||||
}
|
}
|
||||||
m_attachmentPath = QString();
|
m_attachmentPath = QString();
|
||||||
Q_EMIT relationIdChanged();
|
Q_EMIT relationIdChanged(oldEventId, m_relationId);
|
||||||
Q_EMIT attachmentPathChanged();
|
Q_EMIT attachmentPathChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,9 +153,9 @@ void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
|
|||||||
}
|
}
|
||||||
m_attachmentPath = attachmentPath;
|
m_attachmentPath = attachmentPath;
|
||||||
m_relationType = None;
|
m_relationType = None;
|
||||||
m_relationId = QString();
|
const auto oldEventId = std::exchange(m_relationId, QString());
|
||||||
Q_EMIT attachmentPathChanged();
|
Q_EMIT attachmentPathChanged();
|
||||||
Q_EMIT relationIdChanged();
|
Q_EMIT relationIdChanged(oldEventId, m_relationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<Mention> *ChatBarCache::mentions()
|
QList<Mention> *ChatBarCache::mentions()
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ public:
|
|||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void textChanged();
|
void textChanged();
|
||||||
void relationIdChanged();
|
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
|
||||||
void threadIdChanged();
|
void threadIdChanged();
|
||||||
void attachmentPathChanged();
|
void attachmentPathChanged();
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,13 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include <Quotient/events/encryptedevent.h>
|
||||||
|
#include <Quotient/events/roomevent.h>
|
||||||
|
#include <Quotient/events/roommessageevent.h>
|
||||||
|
#include <Quotient/events/stickerevent.h>
|
||||||
|
|
||||||
|
#include "events/pollevent.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class DelegateType
|
* @class DelegateType
|
||||||
*
|
*
|
||||||
@@ -26,23 +33,34 @@ public:
|
|||||||
* similar to the spec it is not the same.
|
* similar to the spec it is not the same.
|
||||||
*/
|
*/
|
||||||
enum Type {
|
enum Type {
|
||||||
Emote, /**< A message that begins with /me. */
|
|
||||||
Notice, /**< A notice event. */
|
|
||||||
Image, /**< A message that is an image. */
|
|
||||||
Audio, /**< A message that is an audio recording. */
|
|
||||||
Video, /**< A message that is a video. */
|
|
||||||
File, /**< A message that is a file. */
|
|
||||||
Message, /**< A text message. */
|
Message, /**< A text message. */
|
||||||
Sticker, /**< A message that is a sticker. */
|
|
||||||
State, /**< A state event in the room. */
|
State, /**< A state event in the room. */
|
||||||
Encrypted, /**< An encrypted message that cannot be decrypted. */
|
|
||||||
ReadMarker, /**< The local user read marker. */
|
ReadMarker, /**< The local user read marker. */
|
||||||
Poll, /**< The initial event for a poll. */
|
|
||||||
Location, /**< A location event. */
|
|
||||||
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
|
|
||||||
Loading, /**< A delegate to tell the user more messages are being loaded. */
|
Loading, /**< A delegate to tell the user more messages are being loaded. */
|
||||||
TimelineEnd, /**< A delegate to inform that all messages are loaded. */
|
TimelineEnd, /**< A delegate to inform that all messages are loaded. */
|
||||||
Other, /**< Anything that cannot be classified as another type. */
|
Other, /**< Anything that cannot be classified as another type. */
|
||||||
};
|
};
|
||||||
Q_ENUM(Type);
|
Q_ENUM(Type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the delegate type for the given event.
|
||||||
|
*
|
||||||
|
* @param event the event to return a type for.
|
||||||
|
*
|
||||||
|
* @sa Type
|
||||||
|
*/
|
||||||
|
static Type typeForEvent(const Quotient::RoomEvent &event)
|
||||||
|
{
|
||||||
|
if (event.is<Quotient::RoomMessageEvent>() || event.is<Quotient::StickerEvent>() || event.is<Quotient::EncryptedEvent>()
|
||||||
|
|| event.is<Quotient::PollStartEvent>()) {
|
||||||
|
return Message;
|
||||||
|
}
|
||||||
|
if (event.isStateEvent()) {
|
||||||
|
if (event.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
||||||
|
return Message;
|
||||||
|
}
|
||||||
|
return State;
|
||||||
|
}
|
||||||
|
return Other;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
107
src/enums/messagecomponenttype.h
Normal file
107
src/enums/messagecomponenttype.h
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include <Quotient/events/encryptedevent.h>
|
||||||
|
#include <Quotient/events/roomevent.h>
|
||||||
|
#include <Quotient/events/roommessageevent.h>
|
||||||
|
#include <Quotient/events/stickerevent.h>
|
||||||
|
|
||||||
|
#include "events/pollevent.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class MessageComponentType
|
||||||
|
*
|
||||||
|
* This class is designed to define the MessageComponentType enumeration.
|
||||||
|
*/
|
||||||
|
class MessageComponentType : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_UNCREATABLE("")
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief The type of component that is needed for an event.
|
||||||
|
*
|
||||||
|
* @note While similar this is not the matrix event or message type. This is
|
||||||
|
* to tell a QML Bubble what component to use to visualise all or part of
|
||||||
|
* a room message.
|
||||||
|
*/
|
||||||
|
enum Type {
|
||||||
|
Text, /**< A text message. */
|
||||||
|
Image, /**< A message that is an image. */
|
||||||
|
Audio, /**< A message that is an audio recording. */
|
||||||
|
Video, /**< A message that is a video. */
|
||||||
|
File, /**< A message that is a file. */
|
||||||
|
Poll, /**< The initial event for a poll. */
|
||||||
|
Location, /**< A location event. */
|
||||||
|
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
|
||||||
|
Encrypted, /**< An encrypted message that cannot be decrypted. */
|
||||||
|
Reply, /**< A component to show a replied-to message. */
|
||||||
|
ReplyLoad, /**< A loading dialog for a reply. */
|
||||||
|
LinkPreview, /**< A preview of a URL in the message. */
|
||||||
|
LinkPreviewLoad, /**< A loading dialog for a link preview. */
|
||||||
|
Edit, /**< A text edit for editing a message. */
|
||||||
|
Other, /**< Anything that cannot be classified as another type. */
|
||||||
|
};
|
||||||
|
Q_ENUM(Type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the delegate type for the given event.
|
||||||
|
*
|
||||||
|
* @param event the event to return a type for.
|
||||||
|
*
|
||||||
|
* @sa Type
|
||||||
|
*/
|
||||||
|
static Type typeForEvent(const Quotient::RoomEvent &event)
|
||||||
|
{
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
if (const auto e = eventCast<const RoomMessageEvent>(&event)) {
|
||||||
|
switch (e->msgtype()) {
|
||||||
|
case MessageEventType::Emote:
|
||||||
|
return MessageComponentType::Text;
|
||||||
|
case MessageEventType::Notice:
|
||||||
|
return MessageComponentType::Text;
|
||||||
|
case MessageEventType::Image:
|
||||||
|
return MessageComponentType::Image;
|
||||||
|
case MessageEventType::Audio:
|
||||||
|
return MessageComponentType::Audio;
|
||||||
|
case MessageEventType::Video:
|
||||||
|
return MessageComponentType::Video;
|
||||||
|
case MessageEventType::Location:
|
||||||
|
return MessageComponentType::Location;
|
||||||
|
case MessageEventType::File:
|
||||||
|
return MessageComponentType::File;
|
||||||
|
default:
|
||||||
|
return MessageComponentType::Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is<const StickerEvent>(event)) {
|
||||||
|
return MessageComponentType::Image;
|
||||||
|
}
|
||||||
|
if (event.isStateEvent()) {
|
||||||
|
if (event.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
||||||
|
return MessageComponentType::LiveLocation;
|
||||||
|
}
|
||||||
|
return MessageComponentType::Other;
|
||||||
|
}
|
||||||
|
if (is<const EncryptedEvent>(event)) {
|
||||||
|
return MessageComponentType::Encrypted;
|
||||||
|
}
|
||||||
|
if (is<PollStartEvent>(event)) {
|
||||||
|
const auto pollEvent = eventCast<const PollStartEvent>(&event);
|
||||||
|
if (pollEvent->isRedacted()) {
|
||||||
|
return MessageComponentType::Text;
|
||||||
|
}
|
||||||
|
return MessageComponentType::Poll;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MessageComponentType::Other;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -14,18 +14,19 @@
|
|||||||
#include <Quotient/events/roomavatarevent.h>
|
#include <Quotient/events/roomavatarevent.h>
|
||||||
#include <Quotient/events/roomcanonicalaliasevent.h>
|
#include <Quotient/events/roomcanonicalaliasevent.h>
|
||||||
#include <Quotient/events/roommemberevent.h>
|
#include <Quotient/events/roommemberevent.h>
|
||||||
|
#include <Quotient/events/roommessageevent.h>
|
||||||
#include <Quotient/events/roompowerlevelsevent.h>
|
#include <Quotient/events/roompowerlevelsevent.h>
|
||||||
#include <Quotient/events/simplestateevents.h>
|
#include <Quotient/events/simplestateevents.h>
|
||||||
#include <Quotient/events/stickerevent.h>
|
#include <Quotient/events/stickerevent.h>
|
||||||
#include <Quotient/quotient_common.h>
|
#include <Quotient/quotient_common.h>
|
||||||
|
|
||||||
#include "delegatetype.h"
|
|
||||||
#include "eventhandler_logging.h"
|
#include "eventhandler_logging.h"
|
||||||
#include "events/locationbeaconevent.h"
|
#include "events/locationbeaconevent.h"
|
||||||
#include "events/pollevent.h"
|
#include "events/pollevent.h"
|
||||||
#include "events/serveraclevent.h"
|
#include "events/serveraclevent.h"
|
||||||
#include "events/widgetevent.h"
|
#include "events/widgetevent.h"
|
||||||
#include "linkpreviewer.h"
|
#include "linkpreviewer.h"
|
||||||
|
#include "messagecomponenttype.h"
|
||||||
#include "models/reactionmodel.h"
|
#include "models/reactionmodel.h"
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
@@ -50,62 +51,14 @@ QString EventHandler::getId() const
|
|||||||
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
|
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateType::Type EventHandler::getDelegateTypeForEvent(const Quotient::RoomEvent *event) const
|
MessageComponentType::Type EventHandler::messageComponentType() const
|
||||||
{
|
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(event)) {
|
|
||||||
switch (e->msgtype()) {
|
|
||||||
case MessageEventType::Emote:
|
|
||||||
return DelegateType::Emote;
|
|
||||||
case MessageEventType::Notice:
|
|
||||||
return DelegateType::Notice;
|
|
||||||
case MessageEventType::Image:
|
|
||||||
return DelegateType::Image;
|
|
||||||
case MessageEventType::Audio:
|
|
||||||
return DelegateType::Audio;
|
|
||||||
case MessageEventType::Video:
|
|
||||||
return DelegateType::Video;
|
|
||||||
case MessageEventType::Location:
|
|
||||||
return DelegateType::Location;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (e->hasFileContent()) {
|
|
||||||
return DelegateType::File;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DelegateType::Message;
|
|
||||||
}
|
|
||||||
if (is<const StickerEvent>(*event)) {
|
|
||||||
return DelegateType::Sticker;
|
|
||||||
}
|
|
||||||
if (event->isStateEvent()) {
|
|
||||||
if (event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
|
||||||
return DelegateType::LiveLocation;
|
|
||||||
}
|
|
||||||
return DelegateType::State;
|
|
||||||
}
|
|
||||||
if (is<const EncryptedEvent>(*event)) {
|
|
||||||
return DelegateType::Encrypted;
|
|
||||||
}
|
|
||||||
if (is<PollStartEvent>(*event)) {
|
|
||||||
const auto pollEvent = eventCast<const PollStartEvent>(event);
|
|
||||||
if (pollEvent->isRedacted()) {
|
|
||||||
return DelegateType::Message;
|
|
||||||
}
|
|
||||||
return DelegateType::Poll;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DelegateType::Other;
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateType::Type EventHandler::getDelegateType() const
|
|
||||||
{
|
{
|
||||||
if (m_event == nullptr) {
|
if (m_event == nullptr) {
|
||||||
qCWarning(EventHandling) << "getDelegateType called with m_event set to nullptr.";
|
qCWarning(EventHandling) << "messageComponentType called with m_event set to nullptr.";
|
||||||
return DelegateType::Other;
|
return MessageComponentType::Other;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getDelegateTypeForEvent(m_event);
|
return MessageComponentType::typeForEvent(*m_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap EventHandler::getAuthor(bool isPending) const
|
QVariantMap EventHandler::getAuthor(bool isPending) const
|
||||||
@@ -776,22 +729,22 @@ QString EventHandler::getReplyId() const
|
|||||||
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateType::Type EventHandler::getReplyDelegateType() const
|
MessageComponentType::Type EventHandler::replyMessageComponentType() const
|
||||||
{
|
{
|
||||||
if (m_room == nullptr) {
|
if (m_room == nullptr) {
|
||||||
qCWarning(EventHandling) << "getReplyDelegateType called with m_room set to nullptr.";
|
qCWarning(EventHandling) << "replyMessageComponentType called with m_room set to nullptr.";
|
||||||
return DelegateType::Other;
|
return MessageComponentType::Other;
|
||||||
}
|
}
|
||||||
if (m_event == nullptr) {
|
if (m_event == nullptr) {
|
||||||
qCWarning(EventHandling) << "getReplyDelegateType called with m_event set to nullptr.";
|
qCWarning(EventHandling) << "replyMessageComponentType called with m_event set to nullptr.";
|
||||||
return DelegateType::Other;
|
return MessageComponentType::Other;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||||
if (replyEvent == nullptr) {
|
if (replyEvent == nullptr) {
|
||||||
return DelegateType::Other;
|
return MessageComponentType::Other;
|
||||||
}
|
}
|
||||||
return getDelegateTypeForEvent(replyEvent);
|
return MessageComponentType::typeForEvent(*replyEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap EventHandler::getReplyAuthor() const
|
QVariantMap EventHandler::getReplyAuthor() const
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
#include <Quotient/events/roomevent.h>
|
#include <Quotient/events/roomevent.h>
|
||||||
#include <Quotient/events/roommessageevent.h>
|
#include <Quotient/events/roommessageevent.h>
|
||||||
|
|
||||||
#include "enums/delegatetype.h"
|
#include "enums/messagecomponenttype.h"
|
||||||
|
|
||||||
class LinkPreviewer;
|
class LinkPreviewer;
|
||||||
class NeoChatRoom;
|
class NeoChatRoom;
|
||||||
@@ -44,13 +44,9 @@ public:
|
|||||||
QString getId() const;
|
QString getId() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Return the DelegateType of the event.
|
* @brief The MessageComponentType to use to visualise the main event content.
|
||||||
*
|
|
||||||
* @note While similar this is not the matrix event or message type. This is
|
|
||||||
* to tell a QML ListView what delegate to show for each event. So while
|
|
||||||
* similar to the spec it is not the same.
|
|
||||||
*/
|
*/
|
||||||
DelegateType::Type getDelegateType() const;
|
MessageComponentType::Type messageComponentType() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the author of the event in context of the room.
|
* @brief Get the author of the event in context of the room.
|
||||||
@@ -224,13 +220,9 @@ public:
|
|||||||
QString getReplyId() const;
|
QString getReplyId() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Return the DelegateType of the event replied to.
|
* @brief The MessageComponentType to use to visualise the reply content.
|
||||||
*
|
|
||||||
* @note While similar this is not the matrix event or message type. This is
|
|
||||||
* to tell a QML ListView what delegate to show for each event. So while
|
|
||||||
* similar to the spec it is not the same.
|
|
||||||
*/
|
*/
|
||||||
DelegateType::Type getReplyDelegateType() const;
|
MessageComponentType::Type replyMessageComponentType() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the author of the event replied to in context of the room.
|
* @brief Get the author of the event replied to in context of the room.
|
||||||
@@ -386,8 +378,6 @@ private:
|
|||||||
|
|
||||||
KFormat m_format;
|
KFormat m_format;
|
||||||
|
|
||||||
DelegateType::Type getDelegateTypeForEvent(const Quotient::RoomEvent *event) const;
|
|
||||||
|
|
||||||
QString getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const;
|
QString getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const;
|
||||||
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;
|
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event)
|
LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event, QObject *parent)
|
||||||
: QObject(nullptr)
|
: QObject(parent)
|
||||||
, m_currentRoom(room)
|
, m_currentRoom(room)
|
||||||
, m_event(event)
|
, m_event(event)
|
||||||
, m_loaded(false)
|
, m_loaded(false)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class LinkPreviewer : public QObject
|
|||||||
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
|
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit LinkPreviewer(const NeoChatRoom *room = nullptr, const Quotient::RoomMessageEvent *event = nullptr);
|
explicit LinkPreviewer(const NeoChatRoom *room = nullptr, const Quotient::RoomMessageEvent *event = nullptr, QObject *parent = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] QUrl url() const;
|
[[nodiscard]] QUrl url() const;
|
||||||
[[nodiscard]] bool loaded() const;
|
[[nodiscard]] bool loaded() const;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
#include "mediamessagefiltermodel.h"
|
#include "mediamessagefiltermodel.h"
|
||||||
|
|
||||||
|
#include <Quotient/events/roommessageevent.h>
|
||||||
#include <Quotient/room.h>
|
#include <Quotient/room.h>
|
||||||
|
|
||||||
#include "enums/delegatetype.h"
|
|
||||||
#include "messageeventmodel.h"
|
#include "messageeventmodel.h"
|
||||||
#include "messagefiltermodel.h"
|
#include "messagefiltermodel.h"
|
||||||
|
|
||||||
@@ -20,8 +20,8 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
|
|||||||
{
|
{
|
||||||
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||||
|
|
||||||
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image
|
if (index.data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))
|
||||||
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
|
|| index.data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("video"))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -30,9 +30,9 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
|
|||||||
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
if (role == SourceRole) {
|
if (role == SourceRole) {
|
||||||
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
|
if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))) {
|
||||||
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
|
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
|
||||||
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
|
} else if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("video"))) {
|
||||||
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
|
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
|
||||||
|
|
||||||
if (progressInfo.completed()) {
|
if (progressInfo.completed()) {
|
||||||
@@ -48,7 +48,7 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
|||||||
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
|
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
|
||||||
}
|
}
|
||||||
if (role == TypeRole) {
|
if (role == TypeRole) {
|
||||||
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
|
if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))) {
|
||||||
return MediaType::Image;
|
return MediaType::Image;
|
||||||
} else {
|
} else {
|
||||||
return MediaType::Video;
|
return MediaType::Video;
|
||||||
|
|||||||
245
src/models/messagecontentmodel.cpp
Normal file
245
src/models/messagecontentmodel.cpp
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "messagecontentmodel.h"
|
||||||
|
|
||||||
|
#include <Quotient/events/redactionevent.h>
|
||||||
|
#include <Quotient/events/stickerevent.h>
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
#include "chatbarcache.h"
|
||||||
|
#include "enums/messagecomponenttype.h"
|
||||||
|
#include "eventhandler.h"
|
||||||
|
#include "linkpreviewer.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
|
||||||
|
MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoChatRoom *room)
|
||||||
|
: QAbstractListModel(nullptr)
|
||||||
|
, m_room(room)
|
||||||
|
, m_event(event)
|
||||||
|
{
|
||||||
|
if (m_room != nullptr) {
|
||||||
|
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
|
||||||
|
if (m_room != nullptr && m_event != nullptr) {
|
||||||
|
if (m_event->id() == serverEvent->id()) {
|
||||||
|
beginResetModel();
|
||||||
|
m_event = serverEvent;
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
|
||||||
|
if (m_room != nullptr && m_event != nullptr) {
|
||||||
|
if (m_event->id() == newEvent->id()) {
|
||||||
|
beginResetModel();
|
||||||
|
m_event = newEvent;
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::replyLoaded, this, [this](const QString &eventId, const QString &replyId) {
|
||||||
|
Q_UNUSED(eventId)
|
||||||
|
if (m_event != nullptr && m_event != nullptr) {
|
||||||
|
const auto eventHandler = EventHandler(m_room, m_event);
|
||||||
|
if (replyId == eventHandler.getReplyId()) {
|
||||||
|
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
|
||||||
|
beginResetModel();
|
||||||
|
m_components[0] = MessageComponentType::Reply;
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
|
||||||
|
if (m_event != nullptr && m_event != nullptr && eventId == m_event->id()) {
|
||||||
|
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
|
||||||
|
if (m_event != nullptr && m_event != nullptr && eventId == m_event->id()) {
|
||||||
|
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
|
||||||
|
if (m_event != nullptr && m_event != nullptr && eventId == m_event->id()) {
|
||||||
|
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
|
||||||
|
if (m_event != nullptr && m_event != nullptr && eventId == m_event->id()) {
|
||||||
|
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
|
||||||
|
if (m_event != nullptr && (oldEventId == m_event->id() || newEventId == m_event->id())) {
|
||||||
|
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
|
||||||
|
beginResetModel();
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
|
||||||
|
if (LinkPreviewer::hasPreviewableLinks(event)) {
|
||||||
|
m_linkPreviewer = new LinkPreviewer(m_room, event, this);
|
||||||
|
|
||||||
|
connect(m_linkPreviewer, &LinkPreviewer::loadedChanged, [this]() {
|
||||||
|
if (m_linkPreviewer->loaded()) {
|
||||||
|
// 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] = MessageComponentType::LinkPreview;
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
|
||||||
|
|
||||||
|
QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index.row() >= rowCount()) {
|
||||||
|
qDebug() << "MessageContentModel, something's wrong: index.row() >= rowCount()";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
EventHandler eventHandler(m_room, m_event);
|
||||||
|
|
||||||
|
if (role == DisplayRole) {
|
||||||
|
if (m_event->isRedacted()) {
|
||||||
|
auto reason = m_event->redactedBecause()->reason();
|
||||||
|
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
|
||||||
|
: i18n("<i>[This message was deleted: %1]</i>", m_event->redactedBecause()->reason());
|
||||||
|
}
|
||||||
|
return eventHandler.getRichBody();
|
||||||
|
}
|
||||||
|
if (role == ComponentTypeRole) {
|
||||||
|
const auto component = m_components[index.row()];
|
||||||
|
if (component == MessageComponentType::Text && !m_event->id().isEmpty() && m_room->editCache()->editId() == m_event->id()) {
|
||||||
|
return MessageComponentType::Edit;
|
||||||
|
}
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
if (role == EventIdRole) {
|
||||||
|
return eventHandler.getId();
|
||||||
|
}
|
||||||
|
if (role == AuthorRole) {
|
||||||
|
return eventHandler.getAuthor(false);
|
||||||
|
}
|
||||||
|
if (role == MediaInfoRole) {
|
||||||
|
return eventHandler.getMediaInfo();
|
||||||
|
}
|
||||||
|
if (role == FileTransferInfoRole) {
|
||||||
|
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
|
||||||
|
if (event->hasFileContent()) {
|
||||||
|
return QVariant::fromValue(m_room->fileTransferInfo(event->id()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
|
||||||
|
return QVariant::fromValue(m_room->fileTransferInfo(event->id()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (role == LatitudeRole) {
|
||||||
|
return eventHandler.getLatitude();
|
||||||
|
}
|
||||||
|
if (role == LongitudeRole) {
|
||||||
|
return eventHandler.getLongitude();
|
||||||
|
}
|
||||||
|
if (role == AssetRole) {
|
||||||
|
return eventHandler.getLocationAssetType();
|
||||||
|
}
|
||||||
|
if (role == PollHandlerRole) {
|
||||||
|
return QVariant::fromValue<PollHandler *>(m_room->poll(m_event->id()));
|
||||||
|
}
|
||||||
|
if (role == IsReplyRole) {
|
||||||
|
return eventHandler.hasReply();
|
||||||
|
}
|
||||||
|
if (role == ReplyComponentType) {
|
||||||
|
return eventHandler.replyMessageComponentType();
|
||||||
|
}
|
||||||
|
if (role == ReplyEventIdRole) {
|
||||||
|
return eventHandler.getReplyId();
|
||||||
|
}
|
||||||
|
if (role == ReplyAuthorRole) {
|
||||||
|
return eventHandler.getReplyAuthor();
|
||||||
|
}
|
||||||
|
if (role == ReplyDisplayRole) {
|
||||||
|
return eventHandler.getReplyRichBody();
|
||||||
|
}
|
||||||
|
if (role == ReplyMediaInfoRole) {
|
||||||
|
return eventHandler.getReplyMediaInfo();
|
||||||
|
}
|
||||||
|
if (role == LinkPreviewerRole) {
|
||||||
|
if (m_linkPreviewer != nullptr) {
|
||||||
|
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewer);
|
||||||
|
} else {
|
||||||
|
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int MessageContentModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(parent)
|
||||||
|
return m_components.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> MessageContentModel::roleNames() const
|
||||||
|
{
|
||||||
|
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
|
||||||
|
roles[DisplayRole] = "display";
|
||||||
|
roles[ComponentTypeRole] = "componentType";
|
||||||
|
roles[EventIdRole] = "eventId";
|
||||||
|
roles[AuthorRole] = "author";
|
||||||
|
roles[MediaInfoRole] = "mediaInfo";
|
||||||
|
roles[FileTransferInfoRole] = "fileTransferInfo";
|
||||||
|
roles[LatitudeRole] = "latitude";
|
||||||
|
roles[LongitudeRole] = "longitude";
|
||||||
|
roles[AssetRole] = "asset";
|
||||||
|
roles[PollHandlerRole] = "pollHandler";
|
||||||
|
roles[IsReplyRole] = "isReply";
|
||||||
|
roles[ReplyComponentType] = "replyComponentType";
|
||||||
|
roles[ReplyEventIdRole] = "replyEventId";
|
||||||
|
roles[ReplyAuthorRole] = "replyAuthor";
|
||||||
|
roles[ReplyDisplayRole] = "replyDisplay";
|
||||||
|
roles[ReplyMediaInfoRole] = "replyMediaInfo";
|
||||||
|
roles[LinkPreviewerRole] = "linkPreviewer";
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MessageContentModel::updateComponents()
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
m_components.clear();
|
||||||
|
|
||||||
|
EventHandler eventHandler(m_room, m_event);
|
||||||
|
if (eventHandler.hasReply()) {
|
||||||
|
if (m_room->findInTimeline(eventHandler.getReplyId()) == m_room->historyEdge()) {
|
||||||
|
m_components += MessageComponentType::ReplyLoad;
|
||||||
|
m_room->loadReply(m_event->id(), eventHandler.getReplyId());
|
||||||
|
} else {
|
||||||
|
m_components += MessageComponentType::Reply;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_components += eventHandler.messageComponentType();
|
||||||
|
|
||||||
|
if (m_linkPreviewer != nullptr) {
|
||||||
|
if (m_linkPreviewer->loaded()) {
|
||||||
|
m_components += MessageComponentType::LinkPreview;
|
||||||
|
} else {
|
||||||
|
m_components += MessageComponentType::LinkPreviewLoad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
83
src/models/messagecontentmodel.h
Normal file
83
src/models/messagecontentmodel.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include "eventhandler.h"
|
||||||
|
#include "linkpreviewer.h"
|
||||||
|
#include "messagecomponenttype.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class MessageContentModel
|
||||||
|
*
|
||||||
|
* A model to visualise the components of a single RoomMessageEvent.
|
||||||
|
*/
|
||||||
|
class MessageContentModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_UNCREATABLE("")
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Defines the model roles.
|
||||||
|
*/
|
||||||
|
enum Roles {
|
||||||
|
DisplayRole = Qt::DisplayRole, /**< The display text for the message. */
|
||||||
|
ComponentTypeRole, /**< The type of component to visualise the message. */
|
||||||
|
EventIdRole, /**< The matrix event ID of the event. */
|
||||||
|
AuthorRole, /**< The author of the event. */
|
||||||
|
MediaInfoRole, /**< The media info for the event. */
|
||||||
|
FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */
|
||||||
|
LatitudeRole, /**< Latitude for a location event. */
|
||||||
|
LongitudeRole, /**< Longitude for a location event. */
|
||||||
|
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
|
||||||
|
PollHandlerRole, /**< The PollHandler for the event, if any. */
|
||||||
|
|
||||||
|
IsReplyRole, /**< Is the message a reply to another event. */
|
||||||
|
ReplyComponentType, /**< The type of component to visualise the reply message. */
|
||||||
|
ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */
|
||||||
|
ReplyAuthorRole, /**< The author of the event that was replied to. */
|
||||||
|
ReplyDisplayRole, /**< The body of the message that was replied to. */
|
||||||
|
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
|
||||||
|
|
||||||
|
LinkPreviewerRole, /**< The link preview details. */
|
||||||
|
};
|
||||||
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
|
explicit MessageContentModel(const Quotient::RoomEvent *event, NeoChatRoom *room);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the given role value at the given index.
|
||||||
|
*
|
||||||
|
* @sa QAbstractItemModel::data
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Number of rows in the model.
|
||||||
|
*
|
||||||
|
* @sa QAbstractItemModel::rowCount
|
||||||
|
*/
|
||||||
|
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns a mapping from Role enum values to role names.
|
||||||
|
*
|
||||||
|
* @sa Roles, QAbstractItemModel::roleNames()
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
NeoChatRoom *m_room = nullptr;
|
||||||
|
const Quotient::RoomEvent *m_event = nullptr;
|
||||||
|
|
||||||
|
QVector<MessageComponentType::Type> m_components;
|
||||||
|
void updateComponents();
|
||||||
|
|
||||||
|
LinkPreviewer *m_linkPreviewer = nullptr;
|
||||||
|
};
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
#include "messageeventmodel.h"
|
#include "messageeventmodel.h"
|
||||||
#include "linkpreviewer.h"
|
|
||||||
#include "messageeventmodel_logging.h"
|
#include "messageeventmodel_logging.h"
|
||||||
|
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
@@ -10,6 +9,7 @@
|
|||||||
#include <Quotient/connection.h>
|
#include <Quotient/connection.h>
|
||||||
#include <Quotient/csapi/rooms.h>
|
#include <Quotient/csapi/rooms.h>
|
||||||
#include <Quotient/events/redactionevent.h>
|
#include <Quotient/events/redactionevent.h>
|
||||||
|
#include <Quotient/events/roommessageevent.h>
|
||||||
#include <Quotient/events/stickerevent.h>
|
#include <Quotient/events/stickerevent.h>
|
||||||
#include <Quotient/user.h>
|
#include <Quotient/user.h>
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
#include "enums/delegatetype.h"
|
#include "enums/delegatetype.h"
|
||||||
#include "eventhandler.h"
|
#include "eventhandler.h"
|
||||||
#include "events/pollevent.h"
|
#include "events/pollevent.h"
|
||||||
|
#include "linkpreviewer.h"
|
||||||
|
#include "messagecontentmodel.h"
|
||||||
#include "models/reactionmodel.h"
|
#include "models/reactionmodel.h"
|
||||||
#include "texthandler.h"
|
#include "texthandler.h"
|
||||||
|
|
||||||
@@ -31,7 +33,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
{
|
{
|
||||||
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
|
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
|
||||||
roles[DelegateTypeRole] = "delegateType";
|
roles[DelegateTypeRole] = "delegateType";
|
||||||
roles[PlainText] = "plainText";
|
|
||||||
roles[EventIdRole] = "eventId";
|
roles[EventIdRole] = "eventId";
|
||||||
roles[TimeRole] = "time";
|
roles[TimeRole] = "time";
|
||||||
roles[TimeStringRole] = "timeString";
|
roles[TimeStringRole] = "timeString";
|
||||||
@@ -40,15 +41,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[HighlightRole] = "isHighlighted";
|
roles[HighlightRole] = "isHighlighted";
|
||||||
roles[SpecialMarksRole] = "marks";
|
roles[SpecialMarksRole] = "marks";
|
||||||
roles[ProgressInfoRole] = "progressInfo";
|
roles[ProgressInfoRole] = "progressInfo";
|
||||||
roles[ShowLinkPreviewRole] = "showLinkPreview";
|
|
||||||
roles[LinkPreviewRole] = "linkPreview";
|
|
||||||
roles[MediaInfoRole] = "mediaInfo";
|
|
||||||
roles[IsReplyRole] = "isReply";
|
|
||||||
roles[ReplyAuthor] = "replyAuthor";
|
|
||||||
roles[ReplyIdRole] = "replyId";
|
|
||||||
roles[ReplyDelegateTypeRole] = "replyDelegateType";
|
|
||||||
roles[ReplyDisplayRole] = "replyDisplay";
|
|
||||||
roles[ReplyMediaInfoRole] = "replyMediaInfo";
|
|
||||||
roles[IsThreadedRole] = "isThreaded";
|
roles[IsThreadedRole] = "isThreaded";
|
||||||
roles[ThreadRootRole] = "threadRoot";
|
roles[ThreadRootRole] = "threadRoot";
|
||||||
roles[ShowAuthorRole] = "showAuthor";
|
roles[ShowAuthorRole] = "showAuthor";
|
||||||
@@ -64,10 +56,8 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[IsRedactedRole] = "isRedacted";
|
roles[IsRedactedRole] = "isRedacted";
|
||||||
roles[GenericDisplayRole] = "genericDisplay";
|
roles[GenericDisplayRole] = "genericDisplay";
|
||||||
roles[IsPendingRole] = "isPending";
|
roles[IsPendingRole] = "isPending";
|
||||||
roles[LatitudeRole] = "latitude";
|
roles[ContentModelRole] = "contentModel";
|
||||||
roles[LongitudeRole] = "longitude";
|
roles[MediaInfoRole] = "mediaInfo";
|
||||||
roles[AssetRole] = "asset";
|
|
||||||
roles[PollHandlerRole] = "pollHandler";
|
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +86,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
beginResetModel();
|
beginResetModel();
|
||||||
if (m_currentRoom) {
|
if (m_currentRoom) {
|
||||||
m_currentRoom->disconnect(this);
|
m_currentRoom->disconnect(this);
|
||||||
m_linkPreviewers.clear();
|
|
||||||
m_reactionModels.clear();
|
m_reactionModels.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,14 +108,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
room->getPreviousContent(50);
|
room->getPreviousContent(50);
|
||||||
}
|
}
|
||||||
lastReadEventId = room->lastFullyReadEventId();
|
lastReadEventId = room->lastFullyReadEventId();
|
||||||
connect(m_currentRoom, &NeoChatRoom::replyLoaded, this, [this](const auto &eventId, const auto &replyId) {
|
|
||||||
Q_UNUSED(replyId);
|
|
||||||
auto row = eventIdToRow(eventId);
|
|
||||||
if (row == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthor});
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
||||||
for (auto &&event : events) {
|
for (auto &&event : events) {
|
||||||
@@ -238,7 +219,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
moveReadMarker(toEventId);
|
moveReadMarker(toEventId);
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
||||||
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
|
|
||||||
const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
|
const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
|
||||||
if (message != nullptr) {
|
if (message != nullptr) {
|
||||||
createEventObjects(message);
|
createEventObjects(message);
|
||||||
@@ -265,10 +245,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole, ExcessReadMarkersRole});
|
refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole, ExcessReadMarkersRole});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::newFileTransfer, this, &MessageEventModel::refreshEvent);
|
|
||||||
connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent);
|
|
||||||
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
|
|
||||||
connect(m_currentRoom, &Room::fileTransferFailed, this, &MessageEventModel::refreshEvent);
|
|
||||||
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] {
|
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] {
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
endResetModel();
|
endResetModel();
|
||||||
@@ -439,8 +415,6 @@ void MessageEventModel::fetchMore(const QModelIndex &parent)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
|
|
||||||
|
|
||||||
QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||||
{
|
{
|
||||||
if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
|
if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
|
||||||
@@ -492,16 +466,24 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return eventHandler.getRichBody();
|
return eventHandler.getRichBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (role == ContentModelRole) {
|
||||||
|
if (!evt.isStateEvent()) {
|
||||||
|
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&evt, m_currentRoom));
|
||||||
|
}
|
||||||
|
if (evt.isStateEvent()) {
|
||||||
|
if (evt.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
||||||
|
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&evt, m_currentRoom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
if (role == GenericDisplayRole) {
|
if (role == GenericDisplayRole) {
|
||||||
return eventHandler.getGenericBody();
|
return eventHandler.getGenericBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == PlainText) {
|
|
||||||
return eventHandler.getPlainBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == DelegateTypeRole) {
|
if (role == DelegateTypeRole) {
|
||||||
return eventHandler.getDelegateType();
|
return DelegateType::typeForEvent(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == AuthorRole) {
|
if (role == AuthorRole) {
|
||||||
@@ -559,46 +541,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
|
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
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()].data());
|
|
||||||
} else {
|
|
||||||
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == MediaInfoRole) {
|
|
||||||
return eventHandler.getMediaInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == IsReplyRole) {
|
|
||||||
return eventHandler.hasReply();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == ReplyIdRole) {
|
|
||||||
return eventHandler.getReplyId();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == ReplyDelegateTypeRole) {
|
|
||||||
return eventHandler.getReplyDelegateType();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == ReplyAuthor) {
|
|
||||||
return eventHandler.getReplyAuthor();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == ReplyDisplayRole) {
|
|
||||||
return eventHandler.getReplyRichBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == ReplyMediaInfoRole) {
|
|
||||||
return eventHandler.getReplyMediaInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == IsThreadedRole) {
|
if (role == IsThreadedRole) {
|
||||||
return eventHandler.isThreaded();
|
return eventHandler.isThreaded();
|
||||||
}
|
}
|
||||||
@@ -642,18 +584,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == LatitudeRole) {
|
|
||||||
return eventHandler.getLatitude();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == LongitudeRole) {
|
|
||||||
return eventHandler.getLongitude();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == AssetRole) {
|
|
||||||
return eventHandler.getLocationAssetType();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == ReadMarkersRole) {
|
if (role == ReadMarkersRole) {
|
||||||
return eventHandler.getReadMarkers();
|
return eventHandler.getReadMarkers();
|
||||||
}
|
}
|
||||||
@@ -703,8 +633,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return row < static_cast<int>(m_currentRoom->pendingEvents().size());
|
return row < static_cast<int>(m_currentRoom->pendingEvents().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == PollHandlerRole) {
|
if (role == MediaInfoRole) {
|
||||||
return QVariant::fromValue<PollHandler *>(m_currentRoom->poll(evt.id()));
|
return eventHandler.getMediaInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
@@ -724,16 +654,6 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
|
|||||||
{
|
{
|
||||||
auto eventId = event->id();
|
auto eventId = event->id();
|
||||||
|
|
||||||
if (m_linkPreviewers.contains(eventId)) {
|
|
||||||
if (!LinkPreviewer::hasPreviewableLinks(event)) {
|
|
||||||
m_linkPreviewers.remove(eventId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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
|
// ReactionModel handles updates to add and remove reactions, we only need to
|
||||||
// handle adding and removing whole models here.
|
// handle adding and removing whole models here.
|
||||||
if (m_reactionModels.contains(eventId)) {
|
if (m_reactionModels.contains(eventId)) {
|
||||||
@@ -761,7 +681,7 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
|
|||||||
bool MessageEventModel::event(QEvent *event)
|
bool MessageEventModel::event(QEvent *event)
|
||||||
{
|
{
|
||||||
if (event->type() == QEvent::ApplicationPaletteChange) {
|
if (event->type() == QEvent::ApplicationPaletteChange) {
|
||||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyAuthor, ReadMarkersRole});
|
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReadMarkersRole});
|
||||||
}
|
}
|
||||||
return QObject::event(event);
|
return QObject::event(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
enum EventRoles {
|
enum EventRoles {
|
||||||
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
|
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
|
||||||
PlainText, /**< Plain text representation of the message. */
|
|
||||||
EventIdRole, /**< The matrix event ID of the event. */
|
EventIdRole, /**< The matrix event ID of the event. */
|
||||||
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
|
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
|
||||||
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
|
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
|
||||||
@@ -50,18 +49,9 @@ public:
|
|||||||
SpecialMarksRole, /**< Whether the event is hidden or not. */
|
SpecialMarksRole, /**< Whether the event is hidden or not. */
|
||||||
ProgressInfoRole, /**< Progress info when downloading files. */
|
ProgressInfoRole, /**< Progress info when downloading files. */
|
||||||
GenericDisplayRole, /**< A generic string based upon the message type. */
|
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. */
|
MediaInfoRole, /**< The media info for the event. */
|
||||||
|
|
||||||
IsReplyRole, /**< Is the message a reply to another event. */
|
ContentModelRole, /**< The MessageContentModel for the event. */
|
||||||
ReplyAuthor, /**< The author of the event that was replied to. */
|
|
||||||
ReplyIdRole, /**< The matrix ID of the message that was replied to. */
|
|
||||||
ReplyDelegateTypeRole, /**< The delegate type of the message that was replied to. */
|
|
||||||
ReplyDisplayRole, /**< The body of the message that was replied to. */
|
|
||||||
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
|
|
||||||
|
|
||||||
IsThreadedRole,
|
IsThreadedRole,
|
||||||
ThreadRootRole,
|
ThreadRootRole,
|
||||||
@@ -80,10 +70,6 @@ public:
|
|||||||
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
|
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
|
||||||
IsRedactedRole, /**< Whether an event has been deleted. */
|
IsRedactedRole, /**< Whether an event has been deleted. */
|
||||||
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
|
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
|
||||||
LatitudeRole, /**< Latitude for a location event. */
|
|
||||||
LongitudeRole, /**< Longitude for a location event. */
|
|
||||||
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
|
|
||||||
PollHandlerRole, /**< The PollHandler for the event, if any. */
|
|
||||||
LastRole, // Keep this last
|
LastRole, // Keep this last
|
||||||
};
|
};
|
||||||
Q_ENUM(EventRoles)
|
Q_ENUM(EventRoles)
|
||||||
@@ -135,7 +121,6 @@ private:
|
|||||||
bool movingEvent = false;
|
bool movingEvent = false;
|
||||||
KFormat m_format;
|
KFormat m_format;
|
||||||
|
|
||||||
QMap<QString, QSharedPointer<LinkPreviewer>> m_linkPreviewers;
|
|
||||||
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||||
|
|
||||||
[[nodiscard]] int timelineBaseIndex() const;
|
[[nodiscard]] int timelineBaseIndex() const;
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
#include "searchmodel.h"
|
#include "searchmodel.h"
|
||||||
|
|
||||||
|
#include "enums/delegatetype.h"
|
||||||
#include "eventhandler.h"
|
#include "eventhandler.h"
|
||||||
#include "messageeventmodel.h"
|
#include "models/messagecontentmodel.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
@@ -82,8 +83,6 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
|||||||
EventHandler eventHandler(m_room, &event);
|
EventHandler eventHandler(m_room, &event);
|
||||||
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case DisplayRole:
|
|
||||||
return eventHandler.getRichBody();
|
|
||||||
case ShowAuthorRole:
|
case ShowAuthorRole:
|
||||||
return true;
|
return true;
|
||||||
case AuthorRole:
|
case AuthorRole:
|
||||||
@@ -103,22 +102,8 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
|||||||
return false;
|
return false;
|
||||||
case ShowReadMarkersRole:
|
case ShowReadMarkersRole:
|
||||||
return false;
|
return false;
|
||||||
case IsReplyRole:
|
|
||||||
return eventHandler.hasReply();
|
|
||||||
case ReplyIdRole:
|
|
||||||
return eventHandler.hasReply();
|
|
||||||
case ReplyAuthorRole:
|
|
||||||
return eventHandler.getReplyAuthor();
|
|
||||||
case ReplyDelegateTypeRole:
|
|
||||||
return eventHandler.getReplyDelegateType();
|
|
||||||
case ReplyDisplayRole:
|
|
||||||
return eventHandler.getReplyRichBody();
|
|
||||||
case ReplyMediaInfoRole:
|
|
||||||
return eventHandler.getReplyMediaInfo();
|
|
||||||
case IsPendingRole:
|
case IsPendingRole:
|
||||||
return false;
|
return false;
|
||||||
case ShowLinkPreviewRole:
|
|
||||||
return false;
|
|
||||||
case HighlightRole:
|
case HighlightRole:
|
||||||
return eventHandler.isHighlighted();
|
return eventHandler.isHighlighted();
|
||||||
case EventIdRole:
|
case EventIdRole:
|
||||||
@@ -127,6 +112,17 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
|||||||
return eventHandler.isThreaded();
|
return eventHandler.isThreaded();
|
||||||
case ThreadRootRole:
|
case ThreadRootRole:
|
||||||
return eventHandler.threadRoot();
|
return eventHandler.threadRoot();
|
||||||
|
case ContentModelRole: {
|
||||||
|
if (!event.isStateEvent()) {
|
||||||
|
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&event, m_room));
|
||||||
|
}
|
||||||
|
if (event.isStateEvent()) {
|
||||||
|
if (event.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
||||||
|
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&event, m_room));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return DelegateType::Message;
|
return DelegateType::Message;
|
||||||
}
|
}
|
||||||
@@ -144,7 +140,6 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
|||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
{DelegateTypeRole, "delegateType"},
|
{DelegateTypeRole, "delegateType"},
|
||||||
{DisplayRole, "display"},
|
|
||||||
{AuthorRole, "author"},
|
{AuthorRole, "author"},
|
||||||
{ShowSectionRole, "showSection"},
|
{ShowSectionRole, "showSection"},
|
||||||
{SectionRole, "section"},
|
{SectionRole, "section"},
|
||||||
@@ -155,25 +150,15 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
|||||||
{ExcessReadMarkersRole, "excessReadMarkers"},
|
{ExcessReadMarkersRole, "excessReadMarkers"},
|
||||||
{HighlightRole, "isHighlighted"},
|
{HighlightRole, "isHighlighted"},
|
||||||
{ReadMarkersString, "readMarkersString"},
|
{ReadMarkersString, "readMarkersString"},
|
||||||
{PlainTextRole, "plainText"},
|
|
||||||
{VerifiedRole, "verified"},
|
{VerifiedRole, "verified"},
|
||||||
{ProgressInfoRole, "progressInfo"},
|
|
||||||
{ShowReactionsRole, "showReactions"},
|
{ShowReactionsRole, "showReactions"},
|
||||||
{IsReplyRole, "isReply"},
|
|
||||||
{ReplyAuthorRole, "replyAuthor"},
|
|
||||||
{ReplyIdRole, "replyId"},
|
|
||||||
{ReplyDelegateTypeRole, "replyDelegateType"},
|
|
||||||
{ReplyDisplayRole, "replyDisplay"},
|
|
||||||
{ReplyMediaInfoRole, "replyMediaInfo"},
|
|
||||||
{ReactionRole, "reaction"},
|
{ReactionRole, "reaction"},
|
||||||
{ReadMarkersRole, "readMarkers"},
|
{ReadMarkersRole, "readMarkers"},
|
||||||
{IsPendingRole, "isPending"},
|
{IsPendingRole, "isPending"},
|
||||||
{ShowReadMarkersRole, "showReadMarkers"},
|
{ShowReadMarkersRole, "showReadMarkers"},
|
||||||
{MimeTypeRole, "mimeType"},
|
|
||||||
{ShowLinkPreviewRole, "showLinkPreview"},
|
|
||||||
{LinkPreviewRole, "linkPreview"},
|
|
||||||
{IsThreadedRole, "isThreaded"},
|
{IsThreadedRole, "isThreaded"},
|
||||||
{ThreadRootRole, "threadRoot"},
|
{ThreadRootRole, "threadRoot"},
|
||||||
|
{ContentModelRole, "contentModel"},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,19 +174,6 @@ void SearchModel::setRoom(NeoChatRoom *room)
|
|||||||
}
|
}
|
||||||
m_room = room;
|
m_room = room;
|
||||||
Q_EMIT roomChanged();
|
Q_EMIT roomChanged();
|
||||||
|
|
||||||
connect(m_room, &NeoChatRoom::replyLoaded, this, [this](const auto &eventId, const auto &replyId) {
|
|
||||||
Q_UNUSED(replyId);
|
|
||||||
const auto &results = m_result->results;
|
|
||||||
auto it = std::find_if(results.begin(), results.end(), [eventId](const auto &event) {
|
|
||||||
return event.result->id() == eventId;
|
|
||||||
});
|
|
||||||
if (it == results.end()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto row = it - results.begin();
|
|
||||||
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthorRole});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SearchModel::searching() const
|
bool SearchModel::searching() const
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ public:
|
|||||||
* since the same delegates are used.
|
* since the same delegates are used.
|
||||||
*/
|
*/
|
||||||
enum Roles {
|
enum Roles {
|
||||||
DisplayRole = Qt::DisplayRole,
|
DelegateTypeRole = Qt::DisplayRole + 1,
|
||||||
DelegateTypeRole,
|
|
||||||
ShowAuthorRole,
|
ShowAuthorRole,
|
||||||
AuthorRole,
|
AuthorRole,
|
||||||
ShowSectionRole,
|
ShowSectionRole,
|
||||||
@@ -63,25 +62,15 @@ public:
|
|||||||
ExcessReadMarkersRole,
|
ExcessReadMarkersRole,
|
||||||
HighlightRole,
|
HighlightRole,
|
||||||
ReadMarkersString,
|
ReadMarkersString,
|
||||||
PlainTextRole,
|
|
||||||
VerifiedRole,
|
VerifiedRole,
|
||||||
ProgressInfoRole,
|
|
||||||
ShowReactionsRole,
|
ShowReactionsRole,
|
||||||
IsReplyRole,
|
|
||||||
ReplyAuthorRole,
|
|
||||||
ReplyIdRole,
|
|
||||||
ReplyDelegateTypeRole,
|
|
||||||
ReplyDisplayRole,
|
|
||||||
ReplyMediaInfoRole,
|
|
||||||
ReactionRole,
|
ReactionRole,
|
||||||
ReadMarkersRole,
|
ReadMarkersRole,
|
||||||
IsPendingRole,
|
IsPendingRole,
|
||||||
ShowReadMarkersRole,
|
ShowReadMarkersRole,
|
||||||
MimeTypeRole,
|
|
||||||
ShowLinkPreviewRole,
|
|
||||||
LinkPreviewRole,
|
|
||||||
IsThreadedRole,
|
IsThreadedRole,
|
||||||
ThreadRootRole,
|
ThreadRootRole,
|
||||||
|
ContentModelRole,
|
||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
explicit SearchModel(QObject *parent = nullptr);
|
explicit SearchModel(QObject *parent = nullptr);
|
||||||
|
|||||||
168
src/qml/AudioComponent.qml
Normal file
168
src/qml/AudioComponent.qml
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtMultimedia
|
||||||
|
|
||||||
|
import org.kde.coreaddons
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show audio from a message.
|
||||||
|
*/
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||||
|
*/
|
||||||
|
required property NeoChatRoom room
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The matrix ID of the message event.
|
||||||
|
*/
|
||||||
|
required property string eventId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The display text of the message.
|
||||||
|
*/
|
||||||
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The media info for the event.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||||
|
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - width - The width in pixels of the audio media.
|
||||||
|
* - height - The height in pixels of the audio media.
|
||||||
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||||
|
*/
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief FileTransferInfo for any downloading files.
|
||||||
|
*/
|
||||||
|
required property var fileTransferInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the media has been downloaded.
|
||||||
|
*/
|
||||||
|
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||||
|
onDownloadedChanged: if (downloaded) {
|
||||||
|
audio.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
MediaPlayer {
|
||||||
|
id: audio
|
||||||
|
onErrorOccurred: (error, errorString) => console.warn("Audio playback error:" + error + errorString)
|
||||||
|
audioOutput: AudioOutput {}
|
||||||
|
}
|
||||||
|
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "notDownloaded"
|
||||||
|
when: !root.fileTransferInfo.completed && !root.fileTransferInfo.active
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
target: playButton
|
||||||
|
icon.name: "media-playback-start"
|
||||||
|
onClicked: root.room.downloadFile(root.eventId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "downloading"
|
||||||
|
when: root.fileTransferInfo.active && !root.fileTransferInfo.completed
|
||||||
|
PropertyChanges {
|
||||||
|
target: downloadBar
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: playButton
|
||||||
|
icon.name: "media-playback-stop"
|
||||||
|
onClicked: {
|
||||||
|
root.room.cancelFileTransfer(root.eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "paused"
|
||||||
|
when: root.fileTransferInfo.completed && (audio.playbackState === MediaPlayer.StoppedState || audio.playbackState === MediaPlayer.PausedState)
|
||||||
|
PropertyChanges {
|
||||||
|
target: playButton
|
||||||
|
icon.name: "media-playback-start"
|
||||||
|
onClicked: {
|
||||||
|
audio.source = root.progressInfo.localPath;
|
||||||
|
audio.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "playing"
|
||||||
|
when: root.fileTransferInfo.completed && audio.playbackState === MediaPlayer.PlayingState
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
target: playButton
|
||||||
|
|
||||||
|
icon.name: "media-playback-pause"
|
||||||
|
|
||||||
|
onClicked: audio.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: playButton
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: root.display
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.ProgressBar {
|
||||||
|
id: downloadBar
|
||||||
|
visible: false
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 0
|
||||||
|
to: root.mediaInfo.size
|
||||||
|
value: root.fileTransferInfo.progress
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
visible: audio.hasAudio
|
||||||
|
|
||||||
|
QQC2.Slider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 0
|
||||||
|
to: audio.duration
|
||||||
|
value: audio.position
|
||||||
|
onMoved: audio.seek(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
visible: root.maxContentWidth > Kirigami.Units.gridUnit * 12
|
||||||
|
|
||||||
|
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||||
|
visible: audio.hasAudio && root.maxContentWidth < Kirigami.Units.gridUnit * 12
|
||||||
|
|
||||||
|
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls as QQC2
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtMultimedia
|
|
||||||
|
|
||||||
import org.kde.coreaddons
|
|
||||||
import org.kde.kirigami as Kirigami
|
|
||||||
|
|
||||||
import org.kde.neochat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A timeline delegate for an audio message.
|
|
||||||
*
|
|
||||||
* @inherit MessageDelegate
|
|
||||||
*/
|
|
||||||
MessageDelegate {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The media info for the event.
|
|
||||||
*
|
|
||||||
* This should consist of the following:
|
|
||||||
* - source - The mxc URL for the media.
|
|
||||||
* - mimeType - The MIME type of the media (should be audio/xxx for this delegate).
|
|
||||||
* - mimeIcon - The MIME icon name (should be audio-xxx).
|
|
||||||
* - size - The file size in bytes.
|
|
||||||
* - duration - The length in seconds of the audio media.
|
|
||||||
*/
|
|
||||||
required property var mediaInfo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the media has been downloaded.
|
|
||||||
*/
|
|
||||||
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
|
||||||
onDownloadedChanged: audio.play()
|
|
||||||
|
|
||||||
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
|
|
||||||
|
|
||||||
bubbleContent: ColumnLayout {
|
|
||||||
MediaPlayer {
|
|
||||||
id: audio
|
|
||||||
onErrorOccurred: (error, errorString) => console.warn("Audio playback error:" + error + errorString)
|
|
||||||
audioOutput: AudioOutput {}
|
|
||||||
}
|
|
||||||
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
name: "notDownloaded"
|
|
||||||
when: !root.progressInfo.completed && !root.progressInfo.active
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: playButton
|
|
||||||
icon.name: "media-playback-start"
|
|
||||||
onClicked: root.room.downloadFile(root.eventId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "downloading"
|
|
||||||
when: root.progressInfo.active && !root.progressInfo.completed
|
|
||||||
PropertyChanges {
|
|
||||||
target: downloadBar
|
|
||||||
visible: true
|
|
||||||
}
|
|
||||||
PropertyChanges {
|
|
||||||
target: playButton
|
|
||||||
icon.name: "media-playback-stop"
|
|
||||||
onClicked: {
|
|
||||||
root.room.cancelFileTransfer(root.eventId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "paused"
|
|
||||||
when: root.progressInfo.completed && (audio.playbackState === MediaPlayer.StoppedState || audio.playbackState === MediaPlayer.PausedState)
|
|
||||||
PropertyChanges {
|
|
||||||
target: playButton
|
|
||||||
icon.name: "media-playback-start"
|
|
||||||
onClicked: {
|
|
||||||
audio.source = root.progressInfo.localPath;
|
|
||||||
audio.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "playing"
|
|
||||||
when: root.progressInfo.completed && audio.playbackState === MediaPlayer.PlayingState
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: playButton
|
|
||||||
|
|
||||||
icon.name: "media-playback-pause"
|
|
||||||
|
|
||||||
onClicked: audio.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
QQC2.ToolButton {
|
|
||||||
id: playButton
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
text: root.display
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.ProgressBar {
|
|
||||||
id: downloadBar
|
|
||||||
visible: false
|
|
||||||
Layout.fillWidth: true
|
|
||||||
from: 0
|
|
||||||
to: root.mediaInfo.size
|
|
||||||
value: root.progressInfo.progress
|
|
||||||
}
|
|
||||||
RowLayout {
|
|
||||||
visible: audio.hasAudio
|
|
||||||
|
|
||||||
QQC2.Slider {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
from: 0
|
|
||||||
to: audio.duration
|
|
||||||
value: audio.position
|
|
||||||
onMoved: audio.seek(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Label {
|
|
||||||
visible: root.contentMaxWidth > Kirigami.Units.gridUnit * 12
|
|
||||||
|
|
||||||
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
|
||||||
visible: audio.hasAudio && root.contentMaxWidth < Kirigami.Units.gridUnit * 12
|
|
||||||
|
|
||||||
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,6 +22,11 @@ import org.kde.neochat
|
|||||||
QQC2.Control {
|
QQC2.Control {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||||
|
*/
|
||||||
|
required property NeoChatRoom room
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The message author.
|
* @brief The message author.
|
||||||
*
|
*
|
||||||
@@ -61,70 +66,28 @@ QQC2.Control {
|
|||||||
property bool showHighlight: false
|
property bool showHighlight: false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The main delegate content item to show in the bubble.
|
* @brief The model to visualise the content of the message.
|
||||||
*/
|
*/
|
||||||
property Item content
|
required property MessageContentModel contentModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether this message is replying to another.
|
* @brief The ActionsHandler object to use.
|
||||||
*/
|
|
||||||
property bool isReply: false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The matrix ID of the reply event.
|
|
||||||
*/
|
|
||||||
required property var replyId
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The reply author.
|
|
||||||
*
|
*
|
||||||
* This should consist of the following:
|
* This is expected to have the correct room set otherwise messages will be sent
|
||||||
* - id - The matrix ID of the reply author.
|
* to the wrong room.
|
||||||
* - isLocalUser - Whether the reply author is the local user.
|
|
||||||
* - avatarSource - The mxc URL for the reply author's avatar in the current room.
|
|
||||||
* - avatarMediaId - The media ID of the reply author's avatar.
|
|
||||||
* - avatarUrl - The mxc URL for the reply author's avatar.
|
|
||||||
* - displayName - The display name of the reply author.
|
|
||||||
* - display - The name of the reply author.
|
|
||||||
* - color - The color for the reply author.
|
|
||||||
* - object - The Quotient::User object for the reply author.
|
|
||||||
*
|
|
||||||
* @sa Quotient::User
|
|
||||||
*/
|
*/
|
||||||
required property var replyAuthor
|
property ActionsHandler actionsHandler
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The delegate type of the message replied to.
|
|
||||||
*/
|
|
||||||
required property int replyDelegateType
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The display text of the message replied to.
|
|
||||||
*/
|
|
||||||
required property string replyDisplay
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The media info for the reply event.
|
|
||||||
*
|
|
||||||
* This could be an image, audio, video or file.
|
|
||||||
*
|
|
||||||
* This should consist of the following:
|
|
||||||
* - source - The mxc URL for the media.
|
|
||||||
* - mimeType - The MIME type of the media.
|
|
||||||
* - mimeIcon - The MIME icon name.
|
|
||||||
* - size - The file size in bytes.
|
|
||||||
* - duration - The length in seconds of the audio media (audio/video only).
|
|
||||||
* - width - The width in pixels of the audio media (image/video only).
|
|
||||||
* - height - The height in pixels of the audio media (image/video only).
|
|
||||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
|
|
||||||
*/
|
|
||||||
required property var replyMediaInfo
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the bubble background should be shown.
|
* @brief Whether the bubble background should be shown.
|
||||||
*/
|
*/
|
||||||
property alias showBackground: bubbleBackground.visible
|
property alias showBackground: bubbleBackground.visible
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The timeline ListView this component is being used in.
|
||||||
|
*/
|
||||||
|
required property ListView timeline
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The maximum width that the bubble's content can be.
|
* @brief The maximum width that the bubble's content can be.
|
||||||
*/
|
*/
|
||||||
@@ -135,11 +98,26 @@ QQC2.Control {
|
|||||||
*/
|
*/
|
||||||
signal replyClicked(string eventID)
|
signal replyClicked(string eventID)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The user selected text has changed.
|
||||||
|
*/
|
||||||
|
signal selectedTextChanged(string selectedText)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request a context menu be show for the message.
|
||||||
|
*/
|
||||||
|
signal showMessageMenu()
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
contentItem: ColumnLayout {
|
||||||
|
id: contentColumn
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: headerRow
|
||||||
Layout.maximumWidth: root.maxContentWidth
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
implicitHeight: Math.max(nameButton.implicitHeight, timeLabel.implicitHeight)
|
||||||
visible: root.showAuthor
|
visible: root.showAuthor
|
||||||
QQC2.AbstractButton {
|
QQC2.AbstractButton {
|
||||||
|
id: nameButton
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
contentItem: QQC2.Label {
|
contentItem: QQC2.Label {
|
||||||
text: root.author.displayName
|
text: root.author.displayName
|
||||||
@@ -152,6 +130,7 @@ QQC2.Control {
|
|||||||
onClicked: RoomManager.resolveResource(root.author.id, "mention")
|
onClicked: RoomManager.resolveResource(root.author.id, "mention")
|
||||||
}
|
}
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
|
id: timeLabel
|
||||||
text: root.timeString
|
text: root.timeString
|
||||||
horizontalAlignment: Text.AlignRight
|
horizontalAlignment: Text.AlignRight
|
||||||
color: Kirigami.Theme.disabledTextColor
|
color: Kirigami.Theme.disabledTextColor
|
||||||
@@ -164,35 +143,19 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loader {
|
Repeater {
|
||||||
id: replyLoader
|
id: contentRepeater
|
||||||
Layout.fillWidth: true
|
model: root.contentModel
|
||||||
Layout.maximumWidth: root.maxContentWidth
|
delegate: MessageComponentChooser {
|
||||||
|
room: root.room
|
||||||
|
actionsHandler: root.actionsHandler
|
||||||
|
timeline: root.timeline
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
|
||||||
active: root.isReply && root.replyDelegateType !== DelegateType.Other
|
onReplyClicked: (eventId) => {root.replyClicked(eventId)}
|
||||||
visible: active
|
onSelectedTextChanged: (selectedText) => {root.selectedTextChanged(selectedText);}
|
||||||
|
onShowMessageMenu: root.showMessageMenu()
|
||||||
sourceComponent: ReplyComponent {
|
|
||||||
author: root.replyAuthor
|
|
||||||
type: root.replyDelegateType
|
|
||||||
display: root.replyDisplay
|
|
||||||
mediaInfo: root.replyMediaInfo
|
|
||||||
contentMaxWidth: root.maxContentWidth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: replyLoader.item
|
|
||||||
function onReplyClicked() {
|
|
||||||
replyClicked(root.replyId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Item {
|
|
||||||
id: contentParent
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.maximumWidth: root.maxContentWidth
|
|
||||||
implicitWidth: root.content ? root.content.implicitWidth : 0
|
|
||||||
implicitHeight: root.content ? root.content.implicitHeight : 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,12 +183,4 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentChanged: {
|
|
||||||
if (!root.content) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
root.content.parent = contentParent;
|
|
||||||
root.content.anchors.fill = contentParent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/qml/EncryptedComponent.qml
Normal file
30
src/qml/EncryptedComponent.qml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component for an encrypted message that can't be decrypted.
|
||||||
|
*/
|
||||||
|
TextEdit {
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
|
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||||
|
selectionColor: Kirigami.Theme.highlightColor
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize
|
||||||
|
selectByMouse: !Kirigami.Settings.isMobile
|
||||||
|
readOnly: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
textFormat: Text.RichText
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
|
||||||
import org.kde.neochat
|
|
||||||
import org.kde.neochat.config
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A timeline delegate for an encrypted message that can't be decrypted.
|
|
||||||
*
|
|
||||||
* @inherit MessageDelegate
|
|
||||||
*/
|
|
||||||
MessageDelegate {
|
|
||||||
id: encryptedDelegate
|
|
||||||
|
|
||||||
bubbleContent: TextEdit {
|
|
||||||
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
|
||||||
color: Kirigami.Theme.disabledTextColor
|
|
||||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
|
||||||
selectionColor: Kirigami.Theme.highlightColor
|
|
||||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize
|
|
||||||
selectByMouse: !Kirigami.Settings.isMobile
|
|
||||||
readOnly: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
textFormat: Text.RichText
|
|
||||||
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -23,65 +24,9 @@ DelegateChooser {
|
|||||||
delegate: StateDelegate {}
|
delegate: StateDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.Emote
|
|
||||||
delegate: TextDelegate {
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: DelegateType.Message
|
roleValue: DelegateType.Message
|
||||||
delegate: TextDelegate {
|
delegate: MessageDelegate {
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.Notice
|
|
||||||
delegate: TextDelegate {
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.Image
|
|
||||||
delegate: ImageDelegate {
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.Sticker
|
|
||||||
delegate: ImageDelegate {
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.Audio
|
|
||||||
delegate: AudioDelegate {
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.Video
|
|
||||||
delegate: VideoDelegate {
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.File
|
|
||||||
delegate: FileDelegate {
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.Encrypted
|
|
||||||
delegate: EncryptedDelegate {
|
|
||||||
room: root.room
|
room: root.room
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,27 +36,6 @@ DelegateChooser {
|
|||||||
delegate: ReadMarkerDelegate {}
|
delegate: ReadMarkerDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.Poll
|
|
||||||
delegate: PollDelegate {
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.Location
|
|
||||||
delegate: LocationDelegate {
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: DelegateType.LiveLocation
|
|
||||||
delegate: LiveLocationDelegate {
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: DelegateType.Loading
|
roleValue: DelegateType.Loading
|
||||||
delegate: LoadingDelegate {}
|
delegate: LoadingDelegate {}
|
||||||
|
|||||||
302
src/qml/FileComponent.qml
Normal file
302
src/qml/FileComponent.qml
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt.labs.platform
|
||||||
|
import Qt.labs.qmlmodels
|
||||||
|
|
||||||
|
import org.kde.coreaddons
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
import org.kde.neochat.config
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show a file from a message.
|
||||||
|
*/
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||||
|
*/
|
||||||
|
required property NeoChatRoom room
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The matrix ID of the message event.
|
||||||
|
*/
|
||||||
|
required property string eventId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The display text of the message.
|
||||||
|
*/
|
||||||
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The media info for the event.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||||
|
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - width - The width in pixels of the audio media.
|
||||||
|
* - height - The height in pixels of the audio media.
|
||||||
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||||
|
*/
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief FileTransferInfo for any downloading files.
|
||||||
|
*/
|
||||||
|
required property var fileTransferInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the media has been downloaded.
|
||||||
|
*/
|
||||||
|
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||||
|
onDownloadedChanged: {
|
||||||
|
itineraryModel.path = root.fileTransferInfo.localPath
|
||||||
|
if (autoOpenFile) {
|
||||||
|
openSavedFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the file should be automatically opened when downloaded.
|
||||||
|
*/
|
||||||
|
property bool autoOpenFile: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
function saveFileAs() {
|
||||||
|
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||||
|
dialog.open()
|
||||||
|
dialog.currentFile = dialog.folder + "/" + root.room.fileNameToDownload(root.eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSavedFile() {
|
||||||
|
UrlHelper.openUrl(root.fileTransferInfo.localPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "downloadedInstant"
|
||||||
|
when: root.fileTransferInfo.completed && autoOpenFile
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
target: openButton
|
||||||
|
icon.name: "document-open"
|
||||||
|
onClicked: openSavedFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
target: downloadButton
|
||||||
|
icon.name: "download"
|
||||||
|
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||||
|
onClicked: saveFileAs()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "downloaded"
|
||||||
|
when: root.fileTransferInfo.completed && !autoOpenFile
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
target: openButton
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
target: downloadButton
|
||||||
|
icon.name: "document-open"
|
||||||
|
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
||||||
|
onClicked: openSavedFile()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "downloading"
|
||||||
|
when: root.fileTransferInfo.active
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
target: openButton
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
target: sizeLabel
|
||||||
|
text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.fileTransferInfo.progress), Format.formatByteSize(root.fileTransferInfo.total))
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: downloadButton
|
||||||
|
icon.name: "media-playback-stop"
|
||||||
|
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
|
||||||
|
onClicked: root.room.cancelFileTransfer(root.eventId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "raw"
|
||||||
|
when: true
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
target: downloadButton
|
||||||
|
onClicked: root.saveFileAs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
source: root.mediaInfo.mimeIcon
|
||||||
|
fallback: "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.display
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
id: sizeLabel
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: Format.formatByteSize(root.mediaInfo.size)
|
||||||
|
opacity: 0.7
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
id: openButton
|
||||||
|
icon.name: "document-open"
|
||||||
|
onClicked: {
|
||||||
|
autoOpenFile = true;
|
||||||
|
root.room.downloadTempFile(root.eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
id: downloadButton
|
||||||
|
icon.name: "download"
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: fileDialog
|
||||||
|
|
||||||
|
FileDialog {
|
||||||
|
fileMode: FileDialog.SaveFile
|
||||||
|
folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||||
|
onAccepted: {
|
||||||
|
Config.lastSaveDirectory = folder
|
||||||
|
Config.save()
|
||||||
|
if (autoOpenFile) {
|
||||||
|
UrlHelper.copyTo(root.fileTransferInfo.localPath, file)
|
||||||
|
} else {
|
||||||
|
root.room.download(root.eventId, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Repeater {
|
||||||
|
id: itinerary
|
||||||
|
model: ItineraryModel {
|
||||||
|
id: itineraryModel
|
||||||
|
connection: root.room.connection
|
||||||
|
}
|
||||||
|
delegate: DelegateChooser {
|
||||||
|
role: "type"
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: "TrainReservation"
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
QQC2.Label {
|
||||||
|
text: model.name
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: model.coach ? i18n("Coach: %1, Seat: %2", model.coach, model.seat) : ""
|
||||||
|
visible: model.coach
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
ColumnLayout {
|
||||||
|
QQC2.Label {
|
||||||
|
text: model.departureStation + (model.departurePlatform ? (" [" + model.departurePlatform + "]") : "")
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: model.departureTime
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
QQC2.Label {
|
||||||
|
text: model.arrivalStation + (model.arrivalPlatform ? (" [" + model.arrivalPlatform + "]") : "")
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: model.arrivalTime
|
||||||
|
opacity: 0.7
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: "LodgingReservation"
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: model.name
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: i18nc("<start time> - <end time>", "%1 - %2", model.startTime, model.endTime)
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: model.address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.Button {
|
||||||
|
icon.name: "map-globe"
|
||||||
|
text: i18nc("@action", "Send to KDE Itinerary")
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
onClicked: itineraryModel.sendToItinerary()
|
||||||
|
visible: itinerary.count > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls as QQC2
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Qt.labs.platform
|
|
||||||
import Qt.labs.qmlmodels
|
|
||||||
|
|
||||||
import org.kde.coreaddons
|
|
||||||
import org.kde.kirigami as Kirigami
|
|
||||||
|
|
||||||
import org.kde.neochat
|
|
||||||
import org.kde.neochat.config
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A timeline delegate for an file message.
|
|
||||||
*
|
|
||||||
* @inherit MessageDelegate
|
|
||||||
*/
|
|
||||||
MessageDelegate {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The media info for the event.
|
|
||||||
*
|
|
||||||
* This should consist of the following:
|
|
||||||
* - source - The mxc URL for the media.
|
|
||||||
* - mimeType - The MIME type of the media.
|
|
||||||
* - mimeIcon - The MIME icon name.
|
|
||||||
* - size - The file size in bytes.
|
|
||||||
*/
|
|
||||||
required property var mediaInfo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the media has been downloaded.
|
|
||||||
*/
|
|
||||||
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the file should be automatically opened when downloaded.
|
|
||||||
*/
|
|
||||||
property bool autoOpenFile: false
|
|
||||||
|
|
||||||
onDownloadedChanged: {
|
|
||||||
itineraryModel.path = root.progressInfo.localPath;
|
|
||||||
if (autoOpenFile) {
|
|
||||||
openSavedFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
|
|
||||||
|
|
||||||
function saveFileAs() {
|
|
||||||
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay);
|
|
||||||
dialog.open();
|
|
||||||
dialog.currentFile = dialog.folder + "/" + root.room.fileNameToDownload(root.eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSavedFile() {
|
|
||||||
UrlHelper.openUrl(root.progressInfo.localPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
bubbleContent: ColumnLayout {
|
|
||||||
spacing: Kirigami.Units.largeSpacing
|
|
||||||
RowLayout {
|
|
||||||
spacing: Kirigami.Units.largeSpacing
|
|
||||||
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
name: "downloadedInstant"
|
|
||||||
when: root.progressInfo.completed && autoOpenFile
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: openButton
|
|
||||||
icon.name: "document-open"
|
|
||||||
onClicked: openSavedFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: downloadButton
|
|
||||||
icon.name: "download"
|
|
||||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
|
||||||
onClicked: saveFileAs()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "downloaded"
|
|
||||||
when: root.progressInfo.completed && !autoOpenFile
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: openButton
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: downloadButton
|
|
||||||
icon.name: "document-open"
|
|
||||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
|
||||||
onClicked: openSavedFile()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "downloading"
|
|
||||||
when: root.progressInfo.active
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: openButton
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: sizeLabel
|
|
||||||
text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.progressInfo.progress), Format.formatByteSize(root.progressInfo.total))
|
|
||||||
}
|
|
||||||
PropertyChanges {
|
|
||||||
target: downloadButton
|
|
||||||
icon.name: "media-playback-stop"
|
|
||||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
|
|
||||||
onClicked: root.room.cancelFileTransfer(root.eventId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "raw"
|
|
||||||
when: true
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
target: downloadButton
|
|
||||||
onClicked: root.saveFileAs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Kirigami.Icon {
|
|
||||||
source: root.mediaInfo.mimeIcon
|
|
||||||
fallback: "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 0
|
|
||||||
QQC2.Label {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: root.display
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
id: sizeLabel
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: Format.formatByteSize(root.mediaInfo.size)
|
|
||||||
opacity: 0.7
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Button {
|
|
||||||
id: openButton
|
|
||||||
icon.name: "document-open"
|
|
||||||
onClicked: {
|
|
||||||
autoOpenFile = true;
|
|
||||||
root.room.downloadTempFile(root.eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Button {
|
|
||||||
id: downloadButton
|
|
||||||
icon.name: "download"
|
|
||||||
|
|
||||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: fileDialog
|
|
||||||
|
|
||||||
FileDialog {
|
|
||||||
fileMode: FileDialog.SaveFile
|
|
||||||
folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
|
||||||
onAccepted: {
|
|
||||||
Config.lastSaveDirectory = folder;
|
|
||||||
Config.save();
|
|
||||||
if (autoOpenFile) {
|
|
||||||
UrlHelper.copyTo(root.progressInfo.localPath, file);
|
|
||||||
} else {
|
|
||||||
root.room.download(root.eventId, file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Repeater {
|
|
||||||
id: itinerary
|
|
||||||
model: ItineraryModel {
|
|
||||||
id: itineraryModel
|
|
||||||
connection: root.room.connection
|
|
||||||
}
|
|
||||||
delegate: DelegateChooser {
|
|
||||||
role: "type"
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: "TrainReservation"
|
|
||||||
delegate: ColumnLayout {
|
|
||||||
Kirigami.Separator {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
RowLayout {
|
|
||||||
QQC2.Label {
|
|
||||||
text: model.name
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
text: model.coach ? i18n("Coach: %1, Seat: %2", model.coach, model.seat) : ""
|
|
||||||
visible: model.coach
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
ColumnLayout {
|
|
||||||
QQC2.Label {
|
|
||||||
text: model.departureStation + (model.departurePlatform ? (" [" + model.departurePlatform + "]") : "")
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
text: model.departureTime
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
ColumnLayout {
|
|
||||||
QQC2.Label {
|
|
||||||
text: model.arrivalStation + (model.arrivalPlatform ? (" [" + model.arrivalPlatform + "]") : "")
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
text: model.arrivalTime
|
|
||||||
opacity: 0.7
|
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: "LodgingReservation"
|
|
||||||
delegate: ColumnLayout {
|
|
||||||
Kirigami.Separator {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
text: model.name
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
text: i18nc("<start time> - <end time>", "%1 - %2", model.startTime, model.endTime)
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
text: model.address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.Button {
|
|
||||||
icon.name: "map-globe"
|
|
||||||
text: i18nc("@action", "Send to KDE Itinerary")
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
QQC2.ToolTip.text: text
|
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
||||||
onClicked: itineraryModel.sendToItinerary()
|
|
||||||
visible: itinerary.count > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
174
src/qml/ImageComponent.qml
Normal file
174
src/qml/ImageComponent.qml
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
|
||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show the image from a message.
|
||||||
|
*/
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||||
|
*/
|
||||||
|
required property NeoChatRoom room
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The matrix ID of the message event.
|
||||||
|
*/
|
||||||
|
required property string eventId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The display text of the message.
|
||||||
|
*/
|
||||||
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The media info for the event.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||||
|
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - width - The width in pixels of the audio media.
|
||||||
|
* - height - The height in pixels of the audio media.
|
||||||
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||||
|
*/
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief FileTransferInfo for any downloading files.
|
||||||
|
*/
|
||||||
|
required property var fileTransferInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The timeline ListView this component is being used in.
|
||||||
|
*/
|
||||||
|
required property ListView timeline
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
implicitWidth: mediaSizeHelper.currentSize.width
|
||||||
|
implicitHeight: mediaSizeHelper.currentSize.height
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: imageLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
active: !root.mediaInfo.animated
|
||||||
|
sourceComponent: Image {
|
||||||
|
source: root.mediaInfo.source
|
||||||
|
sourceSize.width: mediaSizeHelper.currentSize.width * Screen.devicePixelRatio
|
||||||
|
sourceSize.height: mediaSizeHelper.currentSize.height * Screen.devicePixelRatio
|
||||||
|
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: animatedImageLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
active: root?.mediaInfo.animated ?? false
|
||||||
|
sourceComponent: AnimatedImage {
|
||||||
|
source: root.mediaInfo.source
|
||||||
|
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
|
||||||
|
paused: !applicationWindow().active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: root?.mediaInfo.tempInfo.source ?? ""
|
||||||
|
visible: _private.imageItem.status !== Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: root.display
|
||||||
|
QQC2.ToolTip.visible: hoverHandler.hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: hoverHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
visible: _private.imageItem.status !== Image.Ready
|
||||||
|
|
||||||
|
color: "#BB000000"
|
||||||
|
|
||||||
|
QQC2.ProgressBar {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
width: parent.width * 0.8
|
||||||
|
|
||||||
|
from: 0
|
||||||
|
to: 1.0
|
||||||
|
value: _private.imageItem.progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
|
||||||
|
onTapped: {
|
||||||
|
root.QQC2.ToolTip.hide()
|
||||||
|
if (root.mediaInfo.animated) {
|
||||||
|
_private.imageItem.paused = true
|
||||||
|
}
|
||||||
|
root.timeline.interactive = false
|
||||||
|
// We need to make sure the index is that of the MediaMessageFilterModel.
|
||||||
|
if (root.timeline.model instanceof MessageFilterModel) {
|
||||||
|
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.index))
|
||||||
|
} else {
|
||||||
|
RoomManager.maximizeMedia(root.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadAndOpen() {
|
||||||
|
if (_private.downloaded) {
|
||||||
|
openSavedFile()
|
||||||
|
} else {
|
||||||
|
openOnFinished = true
|
||||||
|
root.room.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.room.fileNameToDownload(root.eventId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSavedFile() {
|
||||||
|
if (UrlHelper.openUrl(root.fileTransferInfo.localPath)) return;
|
||||||
|
if (UrlHelper.openUrl(root.fileTransferInfo.localDir)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaSizeHelper {
|
||||||
|
id: mediaSizeHelper
|
||||||
|
contentMaxWidth: root.maxContentWidth
|
||||||
|
mediaWidth: root?.mediaInfo.width ?? 0
|
||||||
|
mediaHeight: root?.mediaInfo.height ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: _private
|
||||||
|
readonly property var imageItem: root.mediaInfo.animated ? animatedImageLoader.item : imageLoader.item
|
||||||
|
|
||||||
|
// The space available for the component after taking away the border
|
||||||
|
readonly property real downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Window
|
|
||||||
import QtQuick.Controls as QQC2
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQml.Models
|
|
||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
|
||||||
|
|
||||||
import org.kde.neochat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A timeline delegate for an image message.
|
|
||||||
*
|
|
||||||
* @inherit MessageDelegate
|
|
||||||
*/
|
|
||||||
MessageDelegate {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The media info for the event.
|
|
||||||
*
|
|
||||||
* This should consist of the following:
|
|
||||||
* - source - The mxc URL for the media.
|
|
||||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
|
||||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
|
||||||
* - size - The file size in bytes.
|
|
||||||
* - width - The width in pixels of the audio media.
|
|
||||||
* - height - The height in pixels of the audio media.
|
|
||||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
|
||||||
*/
|
|
||||||
required property var mediaInfo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the media has been downloaded.
|
|
||||||
*/
|
|
||||||
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the image should be automatically opened when downloaded.
|
|
||||||
*/
|
|
||||||
property bool openOnFinished: false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The maximum width of the image.
|
|
||||||
*/
|
|
||||||
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The maximum height of the image.
|
|
||||||
*/
|
|
||||||
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
|
|
||||||
|
|
||||||
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
|
|
||||||
|
|
||||||
bubbleContent: Item {
|
|
||||||
id: imageContainer
|
|
||||||
|
|
||||||
property var imageItem: root.mediaInfo.animated ? animatedImageLoader.item : imageLoader.item
|
|
||||||
|
|
||||||
implicitWidth: mediaSizeHelper.currentSize.width
|
|
||||||
implicitHeight: mediaSizeHelper.currentSize.height
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: imageLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
active: !root.mediaInfo.animated
|
|
||||||
sourceComponent: Image {
|
|
||||||
source: root.mediaInfo.source
|
|
||||||
sourceSize.width: mediaSizeHelper.currentSize.width * Screen.devicePixelRatio
|
|
||||||
sourceSize.height: mediaSizeHelper.currentSize.height * Screen.devicePixelRatio
|
|
||||||
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: animatedImageLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
active: root.mediaInfo.animated
|
|
||||||
sourceComponent: AnimatedImage {
|
|
||||||
source: root.mediaInfo.source
|
|
||||||
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
|
|
||||||
paused: !applicationWindow().active
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: root.mediaInfo.tempInfo.source
|
|
||||||
visible: imageContainer.imageItem.status !== Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.ToolTip.text: root.display
|
|
||||||
QQC2.ToolTip.visible: hoverHandler.hovered
|
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
||||||
|
|
||||||
HoverHandler {
|
|
||||||
id: hoverHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
visible: (root.progressInfo.active && !downloaded) || imageContainer.imageItem.status !== Image.Ready
|
|
||||||
|
|
||||||
color: "#BB000000"
|
|
||||||
|
|
||||||
QQC2.ProgressBar {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
width: parent.width * 0.8
|
|
||||||
|
|
||||||
from: 0
|
|
||||||
to: root.progressInfo.total
|
|
||||||
value: root.progressInfo.progress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TapHandler {
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
|
|
||||||
onTapped: {
|
|
||||||
imageContainer.QQC2.ToolTip.hide();
|
|
||||||
if (root.mediaInfo.animated) {
|
|
||||||
imageContainer.imageItem.paused = true;
|
|
||||||
}
|
|
||||||
root.ListView.view.interactive = false;
|
|
||||||
// We need to make sure the index is that of the MediaMessageFilterModel.
|
|
||||||
if (root.ListView.view.model instanceof MessageFilterModel) {
|
|
||||||
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.index));
|
|
||||||
} else {
|
|
||||||
RoomManager.maximizeMedia(root.index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadAndOpen() {
|
|
||||||
if (downloaded) {
|
|
||||||
openSavedFile();
|
|
||||||
} else {
|
|
||||||
openOnFinished = true;
|
|
||||||
root.room.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.room.fileNameToDownload(root.eventId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSavedFile() {
|
|
||||||
if (UrlHelper.openUrl(root.progressInfo.localPath))
|
|
||||||
return;
|
|
||||||
if (UrlHelper.openUrl(root.progressInfo.localDir))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaSizeHelper {
|
|
||||||
id: mediaSizeHelper
|
|
||||||
contentMaxWidth: root.contentMaxWidth
|
|
||||||
mediaWidth: root.mediaInfo.width
|
|
||||||
mediaHeight: root.mediaInfo.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
131
src/qml/LinkPreviewComponent.qml
Normal file
131
src/qml/LinkPreviewComponent.qml
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023-2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show a link preview from a message.
|
||||||
|
*/
|
||||||
|
QQC2.Control {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The link preview properties.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
required property var linkPreviewer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Standard height for the link preview.
|
||||||
|
*
|
||||||
|
* 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 bool truncated: linkPreviewDescription.truncated || !linkPreviewDescription.visible
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
topPadding: 0
|
||||||
|
bottomPadding: 0
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
id: contentRow
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: separator
|
||||||
|
Layout.fillHeight: true
|
||||||
|
width: Kirigami.Units.smallSpacing
|
||||||
|
color: Kirigami.Theme.highlightColor
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
id: previewImage
|
||||||
|
Layout.preferredWidth: root.defaultHeight
|
||||||
|
Layout.preferredHeight: root.defaultHeight
|
||||||
|
visible: root.linkPreviewer.imageSource.length > 0
|
||||||
|
source: root.linkPreviewer.imageSource
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
id: column
|
||||||
|
implicitWidth: Math.max(linkPreviewTitle.implicitWidth, linkPreviewDescription.implicitWidth)
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
Kirigami.Heading {
|
||||||
|
id: linkPreviewTitle
|
||||||
|
Layout.fillWidth: true
|
||||||
|
level: 3
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
textFormat: Text.RichText
|
||||||
|
text: "<style>
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<a href=\"" + root.linkPreviewer.url + "\">" + (maximizeButton.checked ? root.linkPreviewer.title : titleTextMetrics.elidedText).replace("–", "—") + "</a>"
|
||||||
|
onLinkActivated: RoomManager.resolveResource(link, "join")
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: titleTextMetrics
|
||||||
|
text: root.linkPreviewer.title
|
||||||
|
font: linkPreviewTitle.font
|
||||||
|
elide: Text.ElideRight
|
||||||
|
elideWidth: (linkPreviewTitle.availableWidth()) * 3
|
||||||
|
}
|
||||||
|
|
||||||
|
function availableWidth() {
|
||||||
|
let previewImageWidth = (previewImage.visible ? previewImage.width + contentRow.spacing : 0);
|
||||||
|
return root.maxContentWidth - contentRow.spacing - separator.width - previewImageWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
id: linkPreviewDescription
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumHeight: maximizeButton.checked ? -1 : root.defaultHeight - linkPreviewTitle.height - column.spacing
|
||||||
|
visible: linkPreviewTitle.height + column.spacing <= root.defaultHeight || maximizeButton.checked
|
||||||
|
text: linkPreviewer.description
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
id: maximizeButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
visible: root.hovered && (root.truncated || checked)
|
||||||
|
checkable: true
|
||||||
|
text: checked ? i18n("Shrink preview") : i18n("Expand preview")
|
||||||
|
icon.name: checked ? "go-up" : "go-down"
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
QQC2.ToolTip {
|
||||||
|
text: maximizeButton.text
|
||||||
|
visible: hovered
|
||||||
|
delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
|
|
||||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls as QQC2
|
|
||||||
import QtQuick.Layouts
|
|
||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
|
||||||
|
|
||||||
import org.kde.neochat
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The link preview properties.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
required property var linkPreviewer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Standard height for the link preview.
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the loading indicator should animate if visible.
|
|
||||||
*/
|
|
||||||
property bool indicatorEnabled: false
|
|
||||||
|
|
||||||
visible: active
|
|
||||||
sourceComponent: linkPreviewer && linkPreviewer.loaded ? linkPreviewComponent : loadingComponent
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: linkPreviewComponent
|
|
||||||
QQC2.Control {
|
|
||||||
id: componentRoot
|
|
||||||
property bool truncated: linkPreviewDescription.truncated || !linkPreviewDescription.visible
|
|
||||||
|
|
||||||
leftPadding: 0
|
|
||||||
rightPadding: 0
|
|
||||||
topPadding: 0
|
|
||||||
bottomPadding: 0
|
|
||||||
|
|
||||||
contentItem: RowLayout {
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
width: Kirigami.Units.smallSpacing
|
|
||||||
color: Kirigami.Theme.highlightColor
|
|
||||||
}
|
|
||||||
Image {
|
|
||||||
visible: root.linkPreviewer.imageSource
|
|
||||||
Layout.maximumHeight: root.defaultHeight
|
|
||||||
Layout.maximumWidth: root.defaultHeight
|
|
||||||
source: root.linkPreviewer.imageSource
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
}
|
|
||||||
ColumnLayout {
|
|
||||||
id: column
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
|
||||||
Kirigami.Heading {
|
|
||||||
id: linkPreviewTitle
|
|
||||||
Layout.fillWidth: true
|
|
||||||
level: 3
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
textFormat: Text.RichText
|
|
||||||
text: "<style>
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<a href=\"" + root.linkPreviewer.url + "\">" + (maximizeButton.checked ? root.linkPreviewer.title : titleTextMetrics.elidedText).replace("–", "—") + "</a>"
|
|
||||||
onLinkActivated: RoomManager.resolveResource(link, "join")
|
|
||||||
|
|
||||||
TextMetrics {
|
|
||||||
id: titleTextMetrics
|
|
||||||
text: root.linkPreviewer.title
|
|
||||||
font: linkPreviewTitle.font
|
|
||||||
elide: Text.ElideRight
|
|
||||||
elideWidth: (linkPreviewTitle.width - Kirigami.Units.largeSpacing * 2.5) * 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
id: linkPreviewDescription
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.maximumHeight: maximizeButton.checked ? -1 : root.defaultHeight - linkPreviewTitle.height - column.spacing
|
|
||||||
visible: linkPreviewTitle.height + column.spacing <= root.defaultHeight || maximizeButton.checked
|
|
||||||
text: linkPreviewer.description
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Button {
|
|
||||||
id: maximizeButton
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
visible: componentRoot.hovered && (componentRoot.truncated || checked)
|
|
||||||
checkable: true
|
|
||||||
text: checked ? i18n("Shrink preview") : i18n("Expand preview")
|
|
||||||
icon.name: checked ? "go-up" : "go-down"
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
|
|
||||||
QQC2.ToolTip {
|
|
||||||
text: maximizeButton.text
|
|
||||||
visible: hovered
|
|
||||||
delay: Kirigami.Units.toolTipDelay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: loadingComponent
|
|
||||||
RowLayout {
|
|
||||||
id: componentRoot
|
|
||||||
property bool truncated: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
width: Kirigami.Units.smallSpacing
|
|
||||||
color: Kirigami.Theme.highlightColor
|
|
||||||
}
|
|
||||||
QQC2.BusyIndicator {
|
|
||||||
running: root.indicatorEnabled
|
|
||||||
}
|
|
||||||
Kirigami.Heading {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.minimumHeight: root.defaultHeight
|
|
||||||
level: 2
|
|
||||||
text: i18n("Loading URL preview")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
92
src/qml/LiveLocationComponent.qml
Normal file
92
src/qml/LiveLocationComponent.qml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtLocation
|
||||||
|
import QtPositioning
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show a live location from a message.
|
||||||
|
*/
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||||
|
*/
|
||||||
|
required property NeoChatRoom room
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The matrix ID of the message event.
|
||||||
|
*/
|
||||||
|
required property string eventId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The display text of the message.
|
||||||
|
*/
|
||||||
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
LiveLocationsModel {
|
||||||
|
id: liveLocationModel
|
||||||
|
eventId: root.eventId
|
||||||
|
room: root.room
|
||||||
|
}
|
||||||
|
MapView {
|
||||||
|
id: mapView
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredWidth: root.maxContentWidth
|
||||||
|
Layout.preferredHeight: root.maxContentWidth / 16 * 9
|
||||||
|
|
||||||
|
map.center: QtPositioning.coordinate(liveLocationModel.boundingBox.y, liveLocationModel.boundingBox.x)
|
||||||
|
map.zoomLevel: 15
|
||||||
|
|
||||||
|
map.plugin: OsmLocationPlugin.plugin
|
||||||
|
|
||||||
|
MapItemView {
|
||||||
|
model: liveLocationModel
|
||||||
|
delegate: LocationMapItem {}
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onTapped: {
|
||||||
|
let map = fullScreenMap.createObject(parent, {liveLocationModel: liveLocationModel});
|
||||||
|
map.open()
|
||||||
|
}
|
||||||
|
onLongPressed: openMessageContext("")
|
||||||
|
}
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
onTapped: openMessageContext("")
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: mapView.map
|
||||||
|
function onCopyrightLinkActivated() {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component {
|
||||||
|
id: fullScreenMap
|
||||||
|
FullScreenMap {}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextComponent {
|
||||||
|
display: root.display
|
||||||
|
visible: root.display !== ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
|
||||||
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtLocation
|
|
||||||
import QtPositioning
|
|
||||||
|
|
||||||
import org.kde.neochat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A timeline delegate for a location message.
|
|
||||||
*
|
|
||||||
* @inherit MessageDelegate
|
|
||||||
*/
|
|
||||||
MessageDelegate {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
bubbleContent: ColumnLayout {
|
|
||||||
LiveLocationsModel {
|
|
||||||
id: liveLocationModel
|
|
||||||
eventId: root.eventId
|
|
||||||
room: root.room
|
|
||||||
}
|
|
||||||
MapView {
|
|
||||||
id: mapView
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: root.contentMaxWidth / 16 * 9
|
|
||||||
|
|
||||||
map.center: QtPositioning.coordinate(liveLocationModel.boundingBox.y, liveLocationModel.boundingBox.x)
|
|
||||||
map.zoomLevel: 15
|
|
||||||
|
|
||||||
map.plugin: OsmLocationPlugin.plugin
|
|
||||||
|
|
||||||
MapItemView {
|
|
||||||
model: liveLocationModel
|
|
||||||
delegate: LocationMapItem {}
|
|
||||||
}
|
|
||||||
|
|
||||||
TapHandler {
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
onTapped: {
|
|
||||||
let map = fullScreenMap.createObject(parent, {
|
|
||||||
liveLocationModel: liveLocationModel
|
|
||||||
});
|
|
||||||
map.open();
|
|
||||||
}
|
|
||||||
onLongPressed: openMessageContext("")
|
|
||||||
}
|
|
||||||
TapHandler {
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
onTapped: openMessageContext("")
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: mapView.map
|
|
||||||
function onCopyrightLinkActivated() {
|
|
||||||
Qt.openUrlExternally(link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component {
|
|
||||||
id: fullScreenMap
|
|
||||||
FullScreenMap {}
|
|
||||||
}
|
|
||||||
|
|
||||||
RichLabel {
|
|
||||||
textMessage: root.display
|
|
||||||
visible: root.display !== ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
60
src/qml/LoadComponent.qml
Normal file
60
src/qml/LoadComponent.qml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show a link preview loading from a message.
|
||||||
|
*/
|
||||||
|
RowLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property int type
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Standard height for the link preview.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
Reply,
|
||||||
|
LinkPreview
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/qml/LocationComponent.qml
Normal file
116
src/qml/LocationComponent.qml
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtLocation
|
||||||
|
import QtPositioning
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show a location from a message.
|
||||||
|
*/
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The message author.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - id - The matrix ID of the author.
|
||||||
|
* - isLocalUser - Whether the author is the local user.
|
||||||
|
* - avatarSource - The mxc URL for the author's avatar in the current room.
|
||||||
|
* - avatarMediaId - The media ID of the author's avatar.
|
||||||
|
* - avatarUrl - The mxc URL for the author's avatar.
|
||||||
|
* - displayName - The display name of the author.
|
||||||
|
* - display - The name of the author.
|
||||||
|
* - color - The color for the author.
|
||||||
|
* - object - The Quotient::User object for the author.
|
||||||
|
*
|
||||||
|
* @sa Quotient::User
|
||||||
|
*/
|
||||||
|
required property var author
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The display text of the message.
|
||||||
|
*/
|
||||||
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The latitude of the location marker in the message.
|
||||||
|
*/
|
||||||
|
required property real latitude
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The longitude of the location marker in the message.
|
||||||
|
*/
|
||||||
|
required property real longitude
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief What type of marker the location message is.
|
||||||
|
*
|
||||||
|
* The main options are m.pin for a general location or m.self for a pin to show
|
||||||
|
* a user's location.
|
||||||
|
*/
|
||||||
|
required property string asset
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
MapView {
|
||||||
|
id: mapView
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredWidth: root.maxContentWidth
|
||||||
|
Layout.preferredHeight: root.maxContentWidth / 16 * 9
|
||||||
|
|
||||||
|
map.center: QtPositioning.coordinate(root.latitude, root.longitude)
|
||||||
|
map.zoomLevel: 15
|
||||||
|
|
||||||
|
map.plugin: OsmLocationPlugin.plugin
|
||||||
|
|
||||||
|
LocationMapItem {
|
||||||
|
latitude: root.latitude
|
||||||
|
longitude: root.longitude
|
||||||
|
asset: root.asset
|
||||||
|
author: root.author
|
||||||
|
isLive: true
|
||||||
|
heading: NaN
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onTapped: {
|
||||||
|
let map = fullScreenMap.createObject(parent, {latitude: root.latitude, longitude: root.longitude, asset: root.asset, author: root.author});
|
||||||
|
map.open()
|
||||||
|
}
|
||||||
|
onLongPressed: openMessageContext("")
|
||||||
|
}
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
onTapped: openMessageContext("")
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: mapView.map
|
||||||
|
function onCopyrightLinkActivated() {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component {
|
||||||
|
id: fullScreenMap
|
||||||
|
FullScreenMap { }
|
||||||
|
}
|
||||||
|
|
||||||
|
TextComponent {
|
||||||
|
display: root.display
|
||||||
|
visible: root.display !== ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtLocation
|
|
||||||
import QtPositioning
|
|
||||||
|
|
||||||
import org.kde.neochat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A timeline delegate for a location message.
|
|
||||||
*
|
|
||||||
* @inherit MessageDelegate
|
|
||||||
*/
|
|
||||||
MessageDelegate {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The latitude of the location marker in the message.
|
|
||||||
*/
|
|
||||||
required property real latitude
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The longitude of the location marker in the message.
|
|
||||||
*/
|
|
||||||
required property real longitude
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief What type of marker the location message is.
|
|
||||||
*
|
|
||||||
* The main options are m.pin for a general location or m.self for a pin to show
|
|
||||||
* a user's location.
|
|
||||||
*/
|
|
||||||
required property string asset
|
|
||||||
|
|
||||||
bubbleContent: ColumnLayout {
|
|
||||||
MapView {
|
|
||||||
id: mapView
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: root.contentMaxWidth / 16 * 9
|
|
||||||
|
|
||||||
map.center: QtPositioning.coordinate(root.latitude, root.longitude)
|
|
||||||
map.zoomLevel: 15
|
|
||||||
|
|
||||||
map.plugin: OsmLocationPlugin.plugin
|
|
||||||
|
|
||||||
LocationMapItem {
|
|
||||||
latitude: root.latitude
|
|
||||||
longitude: root.longitude
|
|
||||||
asset: root.asset
|
|
||||||
author: root.author
|
|
||||||
isLive: true
|
|
||||||
heading: NaN
|
|
||||||
}
|
|
||||||
|
|
||||||
TapHandler {
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
onTapped: {
|
|
||||||
let map = fullScreenMap.createObject(parent, {
|
|
||||||
latitude: root.latitude,
|
|
||||||
longitude: root.longitude,
|
|
||||||
asset: root.asset,
|
|
||||||
author: root.author
|
|
||||||
});
|
|
||||||
map.open();
|
|
||||||
}
|
|
||||||
onLongPressed: openMessageContext("")
|
|
||||||
}
|
|
||||||
TapHandler {
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
onTapped: openMessageContext("")
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: mapView.map
|
|
||||||
function onCopyrightLinkActivated() {
|
|
||||||
Qt.openUrlExternally(link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component {
|
|
||||||
id: fullScreenMap
|
|
||||||
FullScreenMap {}
|
|
||||||
}
|
|
||||||
|
|
||||||
RichLabel {
|
|
||||||
textMessage: root.display
|
|
||||||
visible: root.display !== ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
172
src/qml/MessageComponentChooser.qml
Normal file
172
src/qml/MessageComponentChooser.qml
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Qt.labs.qmlmodels
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select a message component based on a MessageComponentType.
|
||||||
|
*/
|
||||||
|
DelegateChooser {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||||
|
*/
|
||||||
|
required property NeoChatRoom room
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The ActionsHandler object to use.
|
||||||
|
*
|
||||||
|
* This is expected to have the correct room set otherwise messages will be sent
|
||||||
|
* to the wrong room.
|
||||||
|
*/
|
||||||
|
required property ActionsHandler actionsHandler
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The timeline ListView this component is being used in.
|
||||||
|
*/
|
||||||
|
required property ListView timeline
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The reply has been clicked.
|
||||||
|
*/
|
||||||
|
signal replyClicked(string eventID)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The user selected text has changed.
|
||||||
|
*/
|
||||||
|
signal selectedTextChanged(string selectedText)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request a context menu be show for the message.
|
||||||
|
*/
|
||||||
|
signal showMessageMenu()
|
||||||
|
|
||||||
|
role: "componentType"
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Text
|
||||||
|
delegate: TextComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
onSelectedTextChanged: root.selectedTextChanged(selectedText);
|
||||||
|
onShowMessageMenu: root.showMessageMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Image
|
||||||
|
delegate: ImageComponent {
|
||||||
|
room: root.room
|
||||||
|
timeline: root.timeline
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Video
|
||||||
|
delegate: VideoComponent {
|
||||||
|
room: root.room
|
||||||
|
timeline: root.timeline
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Audio
|
||||||
|
delegate: AudioComponent {
|
||||||
|
room: root.room
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.File
|
||||||
|
delegate: FileComponent {
|
||||||
|
room: root.room
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Poll
|
||||||
|
delegate: PollComponent {
|
||||||
|
room: root.room
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Location
|
||||||
|
delegate: LocationComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.LiveLocation
|
||||||
|
delegate: LiveLocationComponent {
|
||||||
|
room: root.room
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Encrypted
|
||||||
|
delegate: EncryptedComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Reply
|
||||||
|
delegate: ReplyComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
onReplyClicked: (eventId) => {root.replyClicked(eventId)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.ReplyLoad
|
||||||
|
delegate: LoadComponent {
|
||||||
|
type: LoadComponent.Reply
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.LinkPreview
|
||||||
|
delegate: LinkPreviewComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.LinkPreviewLoad
|
||||||
|
delegate: LoadComponent {
|
||||||
|
type: LoadComponent.LinkPreview
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Edit
|
||||||
|
delegate: MessageEditComponent {
|
||||||
|
room: root.room
|
||||||
|
actionsHandler: root.actionsHandler
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Other
|
||||||
|
delegate: Item {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,19 +88,9 @@ TimelineDelegate {
|
|||||||
property bool alwaysShowAuthor: false
|
property bool alwaysShowAuthor: false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The delegate type of the message.
|
* @brief The model to visualise the content of the message.
|
||||||
*/
|
*/
|
||||||
required property int delegateType
|
required property MessageContentModel contentModel
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The display text of the message.
|
|
||||||
*/
|
|
||||||
required property string display
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The display text of the message as plain text.
|
|
||||||
*/
|
|
||||||
required property string plainText
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The date of the event as a string.
|
* @brief The date of the event as a string.
|
||||||
@@ -142,65 +132,10 @@ TimelineDelegate {
|
|||||||
*/
|
*/
|
||||||
required property bool showReadMarkers
|
required property bool showReadMarkers
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The matrix ID of the reply event.
|
|
||||||
*/
|
|
||||||
required property var replyId
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The reply author.
|
|
||||||
*
|
|
||||||
* This should consist of the following:
|
|
||||||
* - id - The matrix ID of the reply author.
|
|
||||||
* - isLocalUser - Whether the reply author is the local user.
|
|
||||||
* - avatarSource - The mxc URL for the reply author's avatar in the current room.
|
|
||||||
* - avatarMediaId - The media ID of the reply author's avatar.
|
|
||||||
* - avatarUrl - The mxc URL for the reply author's avatar.
|
|
||||||
* - displayName - The display name of the reply author.
|
|
||||||
* - display - The name of the reply author.
|
|
||||||
* - color - The color for the reply author.
|
|
||||||
* - object - The Quotient::User object for the reply author.
|
|
||||||
*
|
|
||||||
* @sa Quotient::User
|
|
||||||
*/
|
|
||||||
required property var replyAuthor
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The delegate type of the message replied to.
|
|
||||||
*/
|
|
||||||
required property int replyDelegateType
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The display text of the message replied to.
|
|
||||||
*/
|
|
||||||
required property string replyDisplay
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The media info for the reply event.
|
|
||||||
*
|
|
||||||
* This could be an image, audio, video or file.
|
|
||||||
*
|
|
||||||
* This should consist of the following:
|
|
||||||
* - source - The mxc URL for the media.
|
|
||||||
* - mimeType - The MIME type of the media.
|
|
||||||
* - mimeIcon - The MIME icon name.
|
|
||||||
* - size - The file size in bytes.
|
|
||||||
* - duration - The length in seconds of the audio media (audio/video only).
|
|
||||||
* - width - The width in pixels of the audio media (image/video only).
|
|
||||||
* - height - The height in pixels of the audio media (image/video only).
|
|
||||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
|
|
||||||
*/
|
|
||||||
required property var replyMediaInfo
|
|
||||||
|
|
||||||
required property bool isThreaded
|
required property bool isThreaded
|
||||||
|
|
||||||
required property string threadRoot
|
required property string threadRoot
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether this message is replying to another.
|
|
||||||
*/
|
|
||||||
required property bool isReply
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether this message has a local user mention.
|
* @brief Whether this message has a local user mention.
|
||||||
*/
|
*/
|
||||||
@@ -211,13 +146,6 @@ TimelineDelegate {
|
|||||||
*/
|
*/
|
||||||
required property bool isPending
|
required property bool isPending
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Progress info when downloading files.
|
|
||||||
*
|
|
||||||
* @sa Quotient::FileTransferInfo
|
|
||||||
*/
|
|
||||||
required property var progressInfo
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether an encrypted message is sent in a verified session.
|
* @brief Whether an encrypted message is sent in a verified session.
|
||||||
*/
|
*/
|
||||||
@@ -249,11 +177,6 @@ TimelineDelegate {
|
|||||||
*/
|
*/
|
||||||
readonly property alias hovered: bubble.hovered
|
readonly property alias hovered: bubble.hovered
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Open the context menu for the message.
|
|
||||||
*/
|
|
||||||
signal openContextMenu
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Open the any message media externally.
|
* @brief Open the any message media externally.
|
||||||
*/
|
*/
|
||||||
@@ -268,7 +191,7 @@ TimelineDelegate {
|
|||||||
/**
|
/**
|
||||||
* @brief The main delegate content item to show in the bubble.
|
* @brief The main delegate content item to show in the bubble.
|
||||||
*/
|
*/
|
||||||
property alias bubbleContent: bubble.content
|
property var bubbleContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the bubble background is enabled.
|
* @brief Whether the bubble background is enabled.
|
||||||
@@ -293,6 +216,11 @@ TimelineDelegate {
|
|||||||
*/
|
*/
|
||||||
property bool isTemporaryHighlighted: false
|
property bool isTemporaryHighlighted: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The user selected text.
|
||||||
|
*/
|
||||||
|
property string selectedText: ""
|
||||||
|
|
||||||
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) {
|
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) {
|
||||||
temporaryHighlightTimer.start();
|
temporaryHighlightTimer.start();
|
||||||
}
|
}
|
||||||
@@ -329,12 +257,6 @@ TimelineDelegate {
|
|||||||
|
|
||||||
implicitHeight: Math.max(root.showAuthor || root.alwaysShowAuthor ? avatar.implicitHeight : 0, bubble.height)
|
implicitHeight: Math.max(root.showAuthor || root.alwaysShowAuthor ? avatar.implicitHeight : 0, bubble.height)
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (root.isReply && root.replyDelegateType === DelegateType.Other) {
|
|
||||||
root.room.loadReply(root.eventId, root.replyId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// show hover actions
|
// show hover actions
|
||||||
onHoveredChanged: {
|
onHoveredChanged: {
|
||||||
if (hovered && !Kirigami.Settings.isMobile) {
|
if (hovered && !Kirigami.Settings.isMobile) {
|
||||||
@@ -395,23 +317,24 @@ TimelineDelegate {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
room: root.room
|
||||||
|
|
||||||
author: root.author
|
author: root.author
|
||||||
showAuthor: root.showAuthor || root.alwaysShowAuthor
|
showAuthor: root.showAuthor || root.alwaysShowAuthor
|
||||||
time: root.time
|
time: root.time
|
||||||
timeString: root.timeString
|
timeString: root.timeString
|
||||||
|
|
||||||
showHighlight: root.showHighlight
|
contentModel: root.contentModel
|
||||||
|
actionsHandler: root.ListView.view?.actionsHandler ?? null
|
||||||
|
timeline: root.ListView.view
|
||||||
|
|
||||||
isReply: root.isReply
|
showHighlight: root.showHighlight
|
||||||
replyId: root.replyId
|
|
||||||
replyAuthor: root.replyAuthor
|
|
||||||
replyDelegateType: root.replyDelegateType
|
|
||||||
replyDisplay: root.replyDisplay
|
|
||||||
replyMediaInfo: root.replyMediaInfo
|
|
||||||
|
|
||||||
onReplyClicked: eventId => {
|
onReplyClicked: eventId => {
|
||||||
root.replyClicked(eventId);
|
root.replyClicked(eventId);
|
||||||
}
|
}
|
||||||
|
onSelectedTextChanged: (selectedText) => {root.selectedText = selectedText;}
|
||||||
|
onShowMessageMenu: _private.showMessageMenu()
|
||||||
|
|
||||||
showBackground: root.cardBackground && !Config.compactLayout
|
showBackground: root.cardBackground && !Config.compactLayout
|
||||||
}
|
}
|
||||||
@@ -424,12 +347,12 @@ TimelineDelegate {
|
|||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onTapped: root.openContextMenu()
|
onTapped: _private.showMessageMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onLongPressed: root.openContextMenu()
|
onLongPressed: _private.showMessageMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,5 +405,9 @@ TimelineDelegate {
|
|||||||
* @brief Whether local user messages should be aligned right.
|
* @brief Whether local user messages should be aligned right.
|
||||||
*/
|
*/
|
||||||
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !root.alwaysMaxWidth
|
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !root.alwaysMaxWidth
|
||||||
|
|
||||||
|
function showMessageMenu() {
|
||||||
|
RoomManager.viewEventMenu(root.eventId, root.room, root.selectedText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ Loader {
|
|||||||
/**
|
/**
|
||||||
* @brief The delegate type of the message.
|
* @brief The delegate type of the message.
|
||||||
*/
|
*/
|
||||||
required property int delegateType
|
required property int messageComponentType
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The display text of the message as plain text.
|
* @brief The display text of the message as plain text.
|
||||||
@@ -96,7 +96,7 @@ Loader {
|
|||||||
currentRoom.editCache.editId = eventId;
|
currentRoom.editCache.editId = eventId;
|
||||||
currentRoom.mainCache.replyId = "";
|
currentRoom.mainCache.replyId = "";
|
||||||
}
|
}
|
||||||
visible: author.isLocalUser && (root.delegateType === DelegateType.Emote || root.delegateType === DelegateType.Message)
|
visible: author.isLocalUser && (root.messageComponentType === MessageComponentType.Emote || root.messageComponentType === MessageComponentType.Message)
|
||||||
},
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: i18n("Reply")
|
text: i18n("Reply")
|
||||||
|
|||||||
@@ -9,13 +9,19 @@ import org.kde.kirigami as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show an edit text field for a text message being edited.
|
||||||
|
*/
|
||||||
QQC2.TextArea {
|
QQC2.TextArea {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||||
|
*/
|
||||||
required property NeoChatRoom room
|
required property NeoChatRoom room
|
||||||
onRoomChanged: {
|
onRoomChanged: {
|
||||||
_private.chatBarCache = room.editCache;
|
_private.chatBarCache = room.editCache;
|
||||||
_private.chatBarCache.relationIdChanged.connect(_private.updateEditText);
|
_private.chatBarCache.relationIdChanged.connect(_private.updateEditText());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,10 +32,19 @@ QQC2.TextArea {
|
|||||||
*/
|
*/
|
||||||
required property ActionsHandler actionsHandler
|
required property ActionsHandler actionsHandler
|
||||||
|
|
||||||
property string messageId
|
|
||||||
|
|
||||||
property var minimumHeight: editButtons.height + topPadding + bottomPadding
|
property var minimumHeight: editButtons.height + topPadding + bottomPadding
|
||||||
property var preferredWidth: editTextMetrics.advanceWidth + rightPadding + Kirigami.Units.smallSpacing + Kirigami.Units.gridUnit
|
property var preferredWidth: editTextMetrics.advanceWidth + rightPadding + Kirigami.Units.smallSpacing + Kirigami.Units.gridUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
Component.onCompleted: _private.updateEditText()
|
||||||
|
|
||||||
rightPadding: editButtons.width + editButtons.anchors.rightMargin * 2
|
rightPadding: editButtons.width + editButtons.anchors.rightMargin * 2
|
||||||
|
|
||||||
color: Kirigami.Theme.textColor
|
color: Kirigami.Theme.textColor
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import QtQuick.Layouts
|
|||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show media based upon its mime type.
|
||||||
|
*/
|
||||||
RowLayout {
|
RowLayout {
|
||||||
property alias mimeIconSource: icon.source
|
property alias mimeIconSource: icon.source
|
||||||
property alias label: nameLabel.text
|
property alias label: nameLabel.text
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ Components.AlbumMaximizeComponent {
|
|||||||
|
|
||||||
readonly property var currentTime: model.data(model.index(content.currentIndex, 0), MessageEventModel.TimeRole)
|
readonly property var currentTime: model.data(model.index(content.currentIndex, 0), MessageEventModel.TimeRole)
|
||||||
|
|
||||||
readonly property var currentDelegateType: model.data(model.index(content.currentIndex, 0), MessageEventModel.DelegateTypeRole)
|
|
||||||
|
|
||||||
readonly property string currentPlainText: model.data(model.index(content.currentIndex, 0), MessageEventModel.PlainText)
|
|
||||||
|
|
||||||
readonly property var currentMimeType: model.data(model.index(content.currentIndex, 0), MessageEventModel.MimeTypeRole)
|
|
||||||
|
|
||||||
readonly property var currentProgressInfo: model.data(model.index(content.currentIndex, 0), MessageEventModel.ProgressInfoRole)
|
readonly property var currentProgressInfo: model.data(model.index(content.currentIndex, 0), MessageEventModel.ProgressInfoRole)
|
||||||
|
|
||||||
downloadAction: Components.DownloadAction {
|
downloadAction: Components.DownloadAction {
|
||||||
@@ -87,7 +81,8 @@ Components.AlbumMaximizeComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentAuthor, root.currentDelegateType, root.currentPlainText, "", "", root.currentMimeType, root.currentProgressInfo)
|
|
||||||
|
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom)
|
||||||
|
|
||||||
onSaveItem: {
|
onSaveItem: {
|
||||||
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay);
|
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay);
|
||||||
|
|||||||
76
src/qml/PollComponent.qml
Normal file
76
src/qml/PollComponent.qml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt.labs.platform
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show a poll from a message.
|
||||||
|
*/
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||||
|
*/
|
||||||
|
required property NeoChatRoom room
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The matrix ID of the message event.
|
||||||
|
*/
|
||||||
|
required property string eventId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The poll handler for this poll.
|
||||||
|
*
|
||||||
|
* This contains the required information like what the question, answers and
|
||||||
|
* current number of votes for each is.
|
||||||
|
*/
|
||||||
|
required property var pollHandler
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: questionLabel
|
||||||
|
text: root.pollHandler.question
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Repeater {
|
||||||
|
model: root.pollHandler.options
|
||||||
|
delegate: RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
CheckBox {
|
||||||
|
checked: root.pollHandler.answers[root.room.localUser.id] ? root.pollHandler.answers[root.room.localUser.id].includes(modelData["id"]) : false
|
||||||
|
onClicked: root.pollHandler.sendPollAnswer(root.eventId, modelData["id"])
|
||||||
|
enabled: !root.pollHandler.hasEnded
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: modelData["org.matrix.msc1767.text"]
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
visible: root.pollHandler.kind == "org.matrix.msc3381.poll.disclosed" || pollHandler.hasEnded
|
||||||
|
Layout.preferredWidth: contentWidth
|
||||||
|
text: root.pollHandler.counts[modelData["id"]] ?? "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
visible: root.pollHandler.kind == "org.matrix.msc3381.poll.disclosed" || root.pollHandler.hasEnded
|
||||||
|
text: i18np("Based on votes by %1 user", "Based on votes by %1 users", root.pollHandler.answerCount) + (root.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "")
|
||||||
|
font.pointSize: questionLabel.font.pointSize * 0.8
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Qt.labs.platform
|
|
||||||
|
|
||||||
import org.kde.neochat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A timeline delegate for a poll message.
|
|
||||||
*
|
|
||||||
* @inherit MessageDelegate
|
|
||||||
*/
|
|
||||||
MessageDelegate {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The poll handler for this poll.
|
|
||||||
*
|
|
||||||
* This contains the required information like what the question, answers and
|
|
||||||
* current number of votes for each is.
|
|
||||||
*/
|
|
||||||
required property var pollHandler
|
|
||||||
|
|
||||||
bubbleContent: ColumnLayout {
|
|
||||||
Label {
|
|
||||||
id: questionLabel
|
|
||||||
text: root.pollHandler.question
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
Repeater {
|
|
||||||
model: root.pollHandler.options
|
|
||||||
delegate: RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
CheckBox {
|
|
||||||
checked: root.pollHandler.answers[root.room.localUser.id] ? root.pollHandler.answers[root.room.localUser.id].includes(modelData["id"]) : false
|
|
||||||
onClicked: root.pollHandler.sendPollAnswer(root.eventId, modelData["id"])
|
|
||||||
enabled: !root.pollHandler.hasEnded
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
text: modelData["org.matrix.msc1767.text"]
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
visible: root.pollHandler.kind == "org.matrix.msc3381.poll.disclosed" || pollHandler.hasEnded
|
|
||||||
Layout.preferredWidth: contentWidth
|
|
||||||
text: root.pollHandler.counts[modelData["id"]] ?? "0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
visible: root.pollHandler.kind == "org.matrix.msc3381.poll.disclosed" || root.pollHandler.hasEnded
|
|
||||||
text: i18np("Based on votes by %1 user", "Based on votes by %1 users", root.pollHandler.answerCount) + (root.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "")
|
|
||||||
font.pointSize: questionLabel.font.pointSize * 0.8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Qt.labs.qmlmodels
|
||||||
|
|
||||||
import org.kde.coreaddons
|
import org.kde.coreaddons
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
@@ -23,6 +24,16 @@ import org.kde.neochat
|
|||||||
RowLayout {
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The matrix ID of the reply event.
|
||||||
|
*/
|
||||||
|
required property var replyComponentType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The matrix ID of the reply event.
|
||||||
|
*/
|
||||||
|
required property var replyEventId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The reply author.
|
* @brief The reply author.
|
||||||
*
|
*
|
||||||
@@ -39,17 +50,12 @@ RowLayout {
|
|||||||
*
|
*
|
||||||
* @sa Quotient::User
|
* @sa Quotient::User
|
||||||
*/
|
*/
|
||||||
required property var author
|
required property var replyAuthor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The delegate type of the reply message.
|
* @brief The display text of the message replied to.
|
||||||
*/
|
*/
|
||||||
required property int type
|
required property string replyDisplay
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The display text of the message.
|
|
||||||
*/
|
|
||||||
required property string display
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The media info for the reply event.
|
* @brief The media info for the reply event.
|
||||||
@@ -66,15 +72,19 @@ RowLayout {
|
|||||||
* - height - The height in pixels of the audio media (image/video only).
|
* - height - The height in pixels of the audio media (image/video only).
|
||||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
|
||||||
*/
|
*/
|
||||||
required property var mediaInfo
|
required property var replyMediaInfo
|
||||||
|
|
||||||
property real contentMaxWidth
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The reply has been clicked.
|
* @brief The reply has been clicked.
|
||||||
*/
|
*/
|
||||||
signal replyClicked
|
signal replyClicked(string eventID)
|
||||||
|
|
||||||
|
implicitHeight: contentColumn.implicitHeight
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -82,12 +92,17 @@ RowLayout {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
implicitWidth: Kirigami.Units.smallSpacing
|
implicitWidth: Kirigami.Units.smallSpacing
|
||||||
color: root.author.color
|
color: root.replyAuthor.color
|
||||||
}
|
}
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
id: contentColumn
|
||||||
|
implicitHeight: headerRow.implicitHeight + (root.replyComponentType != MessageComponentType.Other ? contentRepeater.itemAt(0).implicitHeight + spacing : 0)
|
||||||
spacing: Kirigami.Units.smallSpacing
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: headerRow
|
||||||
|
implicitHeight: Math.max(replyAvatar.implicitHeight, replyName.implicitHeight)
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
KirigamiComponents.Avatar {
|
KirigamiComponents.Avatar {
|
||||||
@@ -96,42 +111,87 @@ RowLayout {
|
|||||||
implicitWidth: Kirigami.Units.iconSizes.small
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
implicitHeight: Kirigami.Units.iconSizes.small
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
|
||||||
source: root.author.avatarSource
|
source: root.replyAuthor.avatarSource
|
||||||
name: root.author.displayName
|
name: root.replyAuthor.displayName
|
||||||
color: root.author.color
|
color: root.replyAuthor.color
|
||||||
}
|
}
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
id: replyName
|
id: replyName
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
color: root.author.color
|
color: root.replyAuthor.color
|
||||||
text: root.author.displayName
|
text: root.replyAuthor.displayName
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loader {
|
Repeater {
|
||||||
id: loader
|
id: contentRepeater
|
||||||
|
model: [root.replyComponentType]
|
||||||
|
delegate: DelegateChooser {
|
||||||
|
role: "modelData"
|
||||||
|
|
||||||
Layout.fillWidth: true
|
DelegateChoice {
|
||||||
Layout.maximumHeight: loader.item && (root.type == DelegateType.Image || root.type == DelegateType.Sticker) ? loader.item.height : loader.item.implicitHeight
|
roleValue: MessageComponentType.Text
|
||||||
Layout.columnSpan: 2
|
delegate: TextComponent {
|
||||||
|
display: root.replyDisplay
|
||||||
|
maxContentWidth: _private.availableContentWidth
|
||||||
|
|
||||||
sourceComponent: {
|
HoverHandler {
|
||||||
switch (root.type) {
|
enabled: !hoveredLink
|
||||||
case DelegateType.Image:
|
cursorShape: Qt.PointingHandCursor
|
||||||
case DelegateType.Sticker:
|
}
|
||||||
return imageComponent;
|
TapHandler {
|
||||||
case DelegateType.Message:
|
enabled: !hoveredLink
|
||||||
case DelegateType.Notice:
|
acceptedButtons: Qt.LeftButton
|
||||||
return textComponent;
|
onTapped: root.replyClicked(root.replyEventId)
|
||||||
case DelegateType.File:
|
}
|
||||||
case DelegateType.Video:
|
}
|
||||||
case DelegateType.Audio:
|
}
|
||||||
return mimeComponent;
|
DelegateChoice {
|
||||||
case DelegateType.Encrypted:
|
roleValue: MessageComponentType.Image
|
||||||
return encryptedComponent;
|
delegate: Image {
|
||||||
default:
|
id: image
|
||||||
return textComponent;
|
Layout.maximumWidth: mediaSizeHelper.currentSize.width
|
||||||
|
Layout.maximumHeight: mediaSizeHelper.currentSize.height
|
||||||
|
source: root?.replyMediaInfo.source ?? ""
|
||||||
|
|
||||||
|
MediaSizeHelper {
|
||||||
|
id: mediaSizeHelper
|
||||||
|
contentMaxWidth: _private.availableContentWidth
|
||||||
|
mediaWidth: root?.replyMediaInfo.width ?? -1
|
||||||
|
mediaHeight: root?.replyMediaInfo.height ?? -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.File
|
||||||
|
delegate: MimeComponent {
|
||||||
|
mimeIconSource: root.replyMediaInfo.mimeIcon
|
||||||
|
label: root.replyDisplay
|
||||||
|
subLabel: root.replyComponentType === DelegateType.File ? Format.formatByteSize(root.replyMediaInfo.size) : Format.formatDuration(root.replyMediaInfo.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Video
|
||||||
|
delegate: MimeComponent {
|
||||||
|
mimeIconSource: root.replyMediaInfo.mimeIcon
|
||||||
|
label: root.replyDisplay
|
||||||
|
subLabel: root.replyComponentType === DelegateType.File ? Format.formatByteSize(root.replyMediaInfo.size) : Format.formatDuration(root.replyMediaInfo.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Audio
|
||||||
|
delegate: MimeComponent {
|
||||||
|
mimeIconSource: root.replyMediaInfo.mimeIcon
|
||||||
|
label: root.replyDisplay
|
||||||
|
subLabel: root.replyComponentType === DelegateType.File ? Format.formatByteSize(root.replyMediaInfo.size) : Format.formatDuration(root.replyMediaInfo.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Encrypted
|
||||||
|
delegate: TextComponent {
|
||||||
|
display: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,55 +201,11 @@ RowLayout {
|
|||||||
}
|
}
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onTapped: root.replyClicked()
|
onTapped: root.replyClicked(root.replyEventId)
|
||||||
}
|
}
|
||||||
|
QtObject {
|
||||||
Component {
|
id: _private
|
||||||
id: textComponent
|
// The space available for the component after taking away the border
|
||||||
RichLabel {
|
readonly property real availableContentWidth: root.maxContentWidth - verticalBorder.implicitWidth - root.spacing
|
||||||
textMessage: root.display
|
|
||||||
|
|
||||||
HoverHandler {
|
|
||||||
enabled: !hoveredLink
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
}
|
|
||||||
TapHandler {
|
|
||||||
enabled: !hoveredLink
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
onTapped: root.replyClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component {
|
|
||||||
id: imageComponent
|
|
||||||
Image {
|
|
||||||
id: image
|
|
||||||
width: mediaSizeHelper.currentSize.width
|
|
||||||
height: mediaSizeHelper.currentSize.height
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
source: root?.mediaInfo.source ?? ""
|
|
||||||
|
|
||||||
MediaSizeHelper {
|
|
||||||
id: mediaSizeHelper
|
|
||||||
contentMaxWidth: root.contentMaxWidth - verticalBorder.width - root.spacing
|
|
||||||
mediaWidth: root?.mediaInfo.width ?? -1
|
|
||||||
mediaHeight: root?.mediaInfo.height ?? -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component {
|
|
||||||
id: mimeComponent
|
|
||||||
MimeComponent {
|
|
||||||
mimeIconSource: root.mediaInfo.mimeIcon
|
|
||||||
label: root.display
|
|
||||||
subLabel: root.type === DelegateType.File ? Format.formatByteSize(root.mediaInfo.size) : Format.formatDuration(root.mediaInfo.duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component {
|
|
||||||
id: encryptedComponent
|
|
||||||
RichLabel {
|
|
||||||
textMessage: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
|
||||||
textFormat: Text.RichText
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ QQC2.ScrollView {
|
|||||||
role: "type"
|
role: "type"
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: 0//MediaMessageFilterModel.Image
|
roleValue: MediaMessageFilterModel.Image
|
||||||
delegate: ImageDelegate {
|
delegate: MessageDelegate {
|
||||||
alwaysShowAuthor: true
|
alwaysShowAuthor: true
|
||||||
alwaysMaxWidth: true
|
alwaysMaxWidth: true
|
||||||
cardBackground: false
|
cardBackground: false
|
||||||
@@ -57,8 +57,8 @@ QQC2.ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: 1//MediaMessageFilterModel.Video
|
roleValue: MediaMessageFilterModel.Video
|
||||||
delegate: VideoDelegate {
|
delegate: MessageDelegate {
|
||||||
alwaysShowAuthor: true
|
alwaysShowAuthor: true
|
||||||
alwaysMaxWidth: true
|
alwaysMaxWidth: true
|
||||||
cardBackground: false
|
cardBackground: false
|
||||||
|
|||||||
@@ -256,23 +256,23 @@ Kirigami.Page {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onShowMessageMenu(eventId, author, delegateType, plainText, htmlText, selectedText) {
|
function onShowMessageMenu(eventId, author, messageComponentType, plainText, htmlText, selectedText) {
|
||||||
const contextMenu = messageDelegateContextMenu.createObject(root, {
|
const contextMenu = messageDelegateContextMenu.createObject(root, {
|
||||||
selectedText: selectedText,
|
selectedText: selectedText,
|
||||||
author: author,
|
author: author,
|
||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
delegateType: delegateType,
|
messageComponentType: messageComponentType,
|
||||||
plainText: plainText,
|
plainText: plainText,
|
||||||
htmlText: htmlText
|
htmlText: htmlText
|
||||||
});
|
});
|
||||||
contextMenu.open();
|
contextMenu.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onShowFileMenu(eventId, author, delegateType, plainText, mimeType, progressInfo) {
|
function onShowFileMenu(eventId, author, messageComponentType, plainText, mimeType, progressInfo) {
|
||||||
const contextMenu = fileDelegateContextMenu.createObject(root, {
|
const contextMenu = fileDelegateContextMenu.createObject(root, {
|
||||||
author: author,
|
author: author,
|
||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
delegateType: delegateType,
|
messageComponentType: messageComponentType,
|
||||||
plainText: plainText,
|
plainText: plainText,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
progressInfo: progressInfo
|
progressInfo: progressInfo
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
// SPDX-FileCopyrightText: 2020 Black Hat <bhat@encom.eu.org>
|
// SPDX-FileCopyrightText: 2020 Black Hat <bhat@encom.eu.org>
|
||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
import org.kde.neochat
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief A component to show the rich display text of text message.
|
* @brief A component to show rich text from a message.
|
||||||
*/
|
*/
|
||||||
TextEdit {
|
TextEdit {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The rich text message to display.
|
* @brief The display text of the message.
|
||||||
*/
|
*/
|
||||||
property string textMessage
|
required property string display
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether this message is replying to another.
|
* @brief Whether this message is replying to another.
|
||||||
*/
|
*/
|
||||||
property bool isReply
|
property bool isReply: false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Regex for detecting a message with a single emoji.
|
* @brief Regex for detecting a message with a single emoji.
|
||||||
@@ -31,7 +33,7 @@ TextEdit {
|
|||||||
/**
|
/**
|
||||||
* @brief Whether the message is an emoji
|
* @brief Whether the message is an emoji
|
||||||
*/
|
*/
|
||||||
readonly property var isEmoji: isEmojiRegex.test(textMessage)
|
readonly property var isEmoji: isEmojiRegex.test(display)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Regex for detecting a message with a spoiler.
|
* @brief Regex for detecting a message with a spoiler.
|
||||||
@@ -41,9 +43,23 @@ TextEdit {
|
|||||||
/**
|
/**
|
||||||
* @brief Whether a spoiler should be revealed.
|
* @brief Whether a spoiler should be revealed.
|
||||||
*/
|
*/
|
||||||
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
|
property bool spoilerRevealed: !hasSpoiler.test(display)
|
||||||
|
|
||||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request a context menu be show for the message.
|
||||||
|
*/
|
||||||
|
signal showMessageMenu()
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
ListView.onReused: Qt.binding(() => !hasSpoiler.test(display))
|
||||||
|
|
||||||
persistentSelection: true
|
persistentSelection: true
|
||||||
|
|
||||||
@@ -91,7 +107,7 @@ a{
|
|||||||
background: " + Kirigami.Theme.textColor + ";
|
background: " + Kirigami.Theme.textColor + ";
|
||||||
}
|
}
|
||||||
" : "") + "
|
" : "") + "
|
||||||
</style>" + textMessage
|
</style>" + display
|
||||||
|
|
||||||
color: Kirigami.Theme.textColor
|
color: Kirigami.Theme.textColor
|
||||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||||
@@ -106,8 +122,8 @@ a{
|
|||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
|
|
||||||
onLinkActivated: link => {
|
onLinkActivated: link => {
|
||||||
spoilerRevealed = true;
|
spoilerRevealed = true
|
||||||
RoomManager.resolveResource(link, "join");
|
RoomManager.resolveResource(link, "join")
|
||||||
}
|
}
|
||||||
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
|
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
|
||||||
applicationWindow().hoverLinkIndicator.text = hoveredLink;
|
applicationWindow().hoverLinkIndicator.text = hoveredLink;
|
||||||
@@ -116,11 +132,16 @@ a{
|
|||||||
}
|
}
|
||||||
|
|
||||||
HoverHandler {
|
HoverHandler {
|
||||||
cursorShape: (parent.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
|
cursorShape: (root.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
enabled: !parent.hoveredLink && !spoilerRevealed
|
enabled: !root.hoveredLink && !spoilerRevealed
|
||||||
onTapped: spoilerRevealed = true
|
onTapped: spoilerRevealed = true
|
||||||
}
|
}
|
||||||
|
TapHandler {
|
||||||
|
enabled: !root.hoveredLink
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onLongPressed: root.showMessageMenu()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
|
|
||||||
import Qt.labs.qmlmodels
|
|
||||||
|
|
||||||
import org.kde.neochat
|
|
||||||
import org.kde.neochat.config
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A timeline delegate for a text message.
|
|
||||||
*
|
|
||||||
* @inherit MessageDelegate
|
|
||||||
*/
|
|
||||||
MessageDelegate {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The link preview properties.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* @note An empty link previewer should be passed if there are no links to
|
|
||||||
* preview.
|
|
||||||
*/
|
|
||||||
required property var linkPreview
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether there are any links to preview.
|
|
||||||
*/
|
|
||||||
required property bool showLinkPreview
|
|
||||||
|
|
||||||
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, display, label.selectedText)
|
|
||||||
|
|
||||||
bubbleContent: ColumnLayout {
|
|
||||||
RichLabel {
|
|
||||||
id: label
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: root.room.editCache.editId !== root.eventId
|
|
||||||
|
|
||||||
isReply: root.isReply
|
|
||||||
|
|
||||||
textMessage: root.display
|
|
||||||
|
|
||||||
TapHandler {
|
|
||||||
enabled: !label.hoveredLink
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
onLongPressed: root.openContextMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loader {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.minimumHeight: item ? item.minimumHeight : -1
|
|
||||||
Layout.preferredWidth: item ? item.preferredWidth : -1
|
|
||||||
visible: root.room.editCache.editId === root.eventId
|
|
||||||
active: visible
|
|
||||||
sourceComponent: MessageEditComponent {
|
|
||||||
room: root.room
|
|
||||||
actionsHandler: root.ListView.view.actionsHandler
|
|
||||||
messageId: root.eventId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LinkPreviewDelegate {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
active: !root.room.usesEncryption && root.room.urlPreviewEnabled && Config.showLinkPreview && root.showLinkPreview && !root.linkPreview.empty
|
|
||||||
linkPreviewer: root.linkPreview
|
|
||||||
indicatorEnabled: root.isVisibleInTimeline()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
381
src/qml/VideoComponent.qml
Normal file
381
src/qml/VideoComponent.qml
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||||
|
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtMultimedia
|
||||||
|
import Qt.labs.platform as Platform
|
||||||
|
|
||||||
|
import org.kde.coreaddons
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show a video from a message.
|
||||||
|
*/
|
||||||
|
Video {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||||
|
*/
|
||||||
|
required property NeoChatRoom room
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The matrix ID of the message event.
|
||||||
|
*/
|
||||||
|
required property string eventId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The display text of the message.
|
||||||
|
*/
|
||||||
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The media info for the event.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||||
|
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - width - The width in pixels of the audio media.
|
||||||
|
* - height - The height in pixels of the audio media.
|
||||||
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||||
|
*/
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief FileTransferInfo for any downloading files.
|
||||||
|
*/
|
||||||
|
required property var fileTransferInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the media has been downloaded.
|
||||||
|
*/
|
||||||
|
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||||
|
onDownloadedChanged: {
|
||||||
|
if (downloaded) {
|
||||||
|
root.source = root.fileTransferInfo.localPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloaded && playOnFinished) {
|
||||||
|
playSavedFile()
|
||||||
|
playOnFinished = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the video should be played when downloaded.
|
||||||
|
*/
|
||||||
|
property bool playOnFinished: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The timeline ListView this component is being used in.
|
||||||
|
*/
|
||||||
|
required property ListView timeline
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
Layout.preferredWidth: mediaSizeHelper.currentSize.width
|
||||||
|
Layout.preferredHeight: mediaSizeHelper.currentSize.height
|
||||||
|
|
||||||
|
fillMode: VideoOutput.PreserveAspectFit
|
||||||
|
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "notDownloaded"
|
||||||
|
when: !root.fileTransferInfo.completed && !root.fileTransferInfo.active
|
||||||
|
PropertyChanges {
|
||||||
|
target: noDownloadLabel
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: mediaThumbnail
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "downloading"
|
||||||
|
when: root.fileTransferInfo.active && !root.fileTransferInfo.completed
|
||||||
|
PropertyChanges {
|
||||||
|
target: downloadBar
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "paused"
|
||||||
|
when: root.fileTransferInfo.completed && (root.playbackState === MediaPlayer.StoppedState || root.playbackState === MediaPlayer.PausedState)
|
||||||
|
PropertyChanges {
|
||||||
|
target: videoControls
|
||||||
|
stateVisible: true
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: playButton
|
||||||
|
icon.name: "media-playback-start"
|
||||||
|
onClicked: root.play()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "playing"
|
||||||
|
when: root.fileTransferInfo.completed && root.playbackState === MediaPlayer.PlayingState
|
||||||
|
PropertyChanges {
|
||||||
|
target: videoControls
|
||||||
|
stateVisible: true
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: playButton
|
||||||
|
icon.name: "media-playback-pause"
|
||||||
|
onClicked: root.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: mediaThumbnail
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
source: root.mediaInfo.tempInfo.source
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
id: noDownloadLabel
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
color: "white"
|
||||||
|
text: i18n("Video")
|
||||||
|
font.pixelSize: 16
|
||||||
|
|
||||||
|
padding: 8
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Kirigami.Units.smallSpacing
|
||||||
|
color: "black"
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: downloadBar
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
radius: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.ProgressBar {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
width: parent.width * 0.8
|
||||||
|
|
||||||
|
from: 0
|
||||||
|
to: root.fileTransferInfo.total
|
||||||
|
value: root.fileTransferInfo.progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Control {
|
||||||
|
id: videoControls
|
||||||
|
property bool stateVisible: false
|
||||||
|
|
||||||
|
anchors.bottom: root.bottom
|
||||||
|
anchors.left: root.left
|
||||||
|
anchors.right: root.right
|
||||||
|
visible: stateVisible && (videoHoverHandler.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || videoControlTimer.running)
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
id: controlRow
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: playButton
|
||||||
|
}
|
||||||
|
QQC2.Slider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 0
|
||||||
|
to: root.duration
|
||||||
|
value: root.position
|
||||||
|
onMoved: root.seek(value)
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: Format.formatDuration(root.position) + "/" + Format.formatDuration(root.duration)
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: volumeButton
|
||||||
|
property var unmuteVolume: root.volume
|
||||||
|
|
||||||
|
icon.name: root.volume <= 0 ? "player-volume-muted" : "player-volume"
|
||||||
|
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
QQC2.ToolTip.timeout: Kirigami.Units.toolTipDelay
|
||||||
|
QQC2.ToolTip.text: i18nc("@action:button", "Volume")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (root.volume > 0) {
|
||||||
|
root.volume = 0
|
||||||
|
} else {
|
||||||
|
if (unmuteVolume === 0) {
|
||||||
|
root.volume = 1
|
||||||
|
} else {
|
||||||
|
root.volume = unmuteVolume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onHoveredChanged: {
|
||||||
|
if (!hovered && (root.state === "paused" || root.state === "playing")) {
|
||||||
|
videoControlTimer.restart()
|
||||||
|
volumePopupTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Popup {
|
||||||
|
id: volumePopup
|
||||||
|
y: -height
|
||||||
|
width: volumeButton.width
|
||||||
|
visible: videoControls.stateVisible && (volumeButton.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || volumePopupTimer.running)
|
||||||
|
|
||||||
|
focus: true
|
||||||
|
padding: Kirigami.Units.smallSpacing
|
||||||
|
closePolicy: QQC2.Popup.NoAutoClose
|
||||||
|
|
||||||
|
QQC2.Slider {
|
||||||
|
id: volumeSlider
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitHeight: Kirigami.Units.gridUnit * 7
|
||||||
|
orientation: Qt.Vertical
|
||||||
|
padding: 0
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
value: root.volume
|
||||||
|
onMoved: {
|
||||||
|
root.volume = value
|
||||||
|
volumeButton.unmuteVolume = value
|
||||||
|
}
|
||||||
|
onHoveredChanged: {
|
||||||
|
if (!hovered && (root.state === "paused" || root.state === "playing")) {
|
||||||
|
rooteoControlTimer.restart()
|
||||||
|
volumePopupTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
id: volumePopupTimer
|
||||||
|
interval: 500
|
||||||
|
}
|
||||||
|
HoverHandler {
|
||||||
|
id: volumePopupHoverHandler
|
||||||
|
onHoveredChanged: {
|
||||||
|
if (!hovered && (root.state === "paused" || root.state === "playing")) {
|
||||||
|
videoControlTimer.restart()
|
||||||
|
volumePopupTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
background: Kirigami.ShadowedRectangle {
|
||||||
|
radius: 4
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
opacity: 0.8
|
||||||
|
|
||||||
|
property color borderColor: Kirigami.Theme.textColor
|
||||||
|
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
shadow.xOffset: 0
|
||||||
|
shadow.yOffset: 4
|
||||||
|
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
shadow.size: 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: maximizeButton
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
action: Kirigami.Action {
|
||||||
|
text: i18n("Maximize")
|
||||||
|
icon.name: "view-fullscreen"
|
||||||
|
onTriggered: {
|
||||||
|
root.timeline.interactive = false
|
||||||
|
root.pause()
|
||||||
|
// We need to make sure the index is that of the MediaMessageFilterModel.
|
||||||
|
if (root.timeline.model instanceof MessageFilterModel) {
|
||||||
|
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.index))
|
||||||
|
} else {
|
||||||
|
RoomManager.maximizeMedia(root.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
background: Kirigami.ShadowedRectangle {
|
||||||
|
radius: 4
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
opacity: 0.8
|
||||||
|
|
||||||
|
property color borderColor: Kirigami.Theme.textColor
|
||||||
|
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
shadow.xOffset: 0
|
||||||
|
shadow.yOffset: 4
|
||||||
|
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
shadow.size: 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: videoControlTimer
|
||||||
|
interval: 1000
|
||||||
|
}
|
||||||
|
HoverHandler {
|
||||||
|
id: videoHoverHandler
|
||||||
|
onHoveredChanged: {
|
||||||
|
if (!hovered && (root.state === "paused" || root.state === "playing")) {
|
||||||
|
videoControlTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
|
||||||
|
onTapped: if (root.fileTransferInfo.completed) {
|
||||||
|
if (root.playbackState == MediaPlayer.PlayingState) {
|
||||||
|
root.pause()
|
||||||
|
} else {
|
||||||
|
root.play()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
root.downloadAndPlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaSizeHelper {
|
||||||
|
id: mediaSizeHelper
|
||||||
|
contentMaxWidth: root.maxContentWidth
|
||||||
|
mediaWidth: root.mediaInfo.width
|
||||||
|
mediaHeight: root.mediaInfo.height
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadAndPlay() {
|
||||||
|
if (root.downloaded) {
|
||||||
|
playSavedFile()
|
||||||
|
} else {
|
||||||
|
playOnFinished = true
|
||||||
|
root.room.downloadFile(root.eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.room.fileNameToDownload(root.eventId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playSavedFile() {
|
||||||
|
root.stop()
|
||||||
|
root.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls as QQC2
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtMultimedia
|
|
||||||
import Qt.labs.platform as Platform
|
|
||||||
|
|
||||||
import org.kde.coreaddons
|
|
||||||
import org.kde.kirigami as Kirigami
|
|
||||||
|
|
||||||
import org.kde.neochat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A timeline delegate for a video message.
|
|
||||||
*
|
|
||||||
* @inherit MessageDelegate
|
|
||||||
*/
|
|
||||||
MessageDelegate {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The media info for the event.
|
|
||||||
*
|
|
||||||
* This should consist of the following:
|
|
||||||
* - source - The mxc URL for the media.
|
|
||||||
* - mimeType - The MIME type of the media (should be video/xxx for this delegate).
|
|
||||||
* - mimeIcon - The MIME icon name (should be video-xxx).
|
|
||||||
* - size - The file size in bytes.
|
|
||||||
* - duration - The length in seconds of the audio media.
|
|
||||||
* - width - The width in pixels of the audio media.
|
|
||||||
* - height - The height in pixels of the audio media.
|
|
||||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
|
||||||
*/
|
|
||||||
required property var mediaInfo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the media has been downloaded.
|
|
||||||
*/
|
|
||||||
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the video should be played when downloaded.
|
|
||||||
*/
|
|
||||||
property bool playOnFinished: false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The maximum width of the image.
|
|
||||||
*/
|
|
||||||
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The maximum height of the image.
|
|
||||||
*/
|
|
||||||
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
|
|
||||||
|
|
||||||
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
|
|
||||||
|
|
||||||
onDownloadedChanged: {
|
|
||||||
if (downloaded) {
|
|
||||||
vid.source = root.progressInfo.localPath;
|
|
||||||
}
|
|
||||||
if (downloaded && playOnFinished) {
|
|
||||||
playSavedFile();
|
|
||||||
playOnFinished = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bubbleContent: Video {
|
|
||||||
id: vid
|
|
||||||
implicitWidth: mediaSizeHelper.currentSize.width
|
|
||||||
implicitHeight: mediaSizeHelper.currentSize.height
|
|
||||||
|
|
||||||
fillMode: VideoOutput.PreserveAspectFit
|
|
||||||
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
name: "notDownloaded"
|
|
||||||
when: !root.progressInfo.completed && !root.progressInfo.active
|
|
||||||
PropertyChanges {
|
|
||||||
target: noDownloadLabel
|
|
||||||
visible: true
|
|
||||||
}
|
|
||||||
PropertyChanges {
|
|
||||||
target: mediaThumbnail
|
|
||||||
visible: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "downloading"
|
|
||||||
when: root.progressInfo.active && !root.progressInfo.completed
|
|
||||||
PropertyChanges {
|
|
||||||
target: downloadBar
|
|
||||||
visible: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "paused"
|
|
||||||
when: root.progressInfo.completed && (vid.playbackState === MediaPlayer.StoppedState || vid.playbackState === MediaPlayer.PausedState)
|
|
||||||
PropertyChanges {
|
|
||||||
target: videoControls
|
|
||||||
stateVisible: true
|
|
||||||
}
|
|
||||||
PropertyChanges {
|
|
||||||
target: playButton
|
|
||||||
icon.name: "media-playback-start"
|
|
||||||
onClicked: vid.play()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "playing"
|
|
||||||
when: root.progressInfo.completed && vid.playbackState === MediaPlayer.PlayingState
|
|
||||||
PropertyChanges {
|
|
||||||
target: videoControls
|
|
||||||
stateVisible: true
|
|
||||||
}
|
|
||||||
PropertyChanges {
|
|
||||||
target: playButton
|
|
||||||
icon.name: "media-playback-pause"
|
|
||||||
onClicked: vid.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: mediaThumbnail
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
source: root.mediaInfo.tempInfo.source
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Label {
|
|
||||||
id: noDownloadLabel
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
color: "white"
|
|
||||||
text: i18n("Video")
|
|
||||||
font.pixelSize: 16
|
|
||||||
|
|
||||||
padding: 8
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
radius: Kirigami.Units.smallSpacing
|
|
||||||
color: "black"
|
|
||||||
opacity: 0.3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: downloadBar
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
color: Kirigami.Theme.backgroundColor
|
|
||||||
radius: Kirigami.Units.smallSpacing
|
|
||||||
|
|
||||||
QQC2.ProgressBar {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
width: parent.width * 0.8
|
|
||||||
|
|
||||||
from: 0
|
|
||||||
to: root.progressInfo.total
|
|
||||||
value: root.progressInfo.progress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Control {
|
|
||||||
id: videoControls
|
|
||||||
property bool stateVisible: false
|
|
||||||
|
|
||||||
anchors.bottom: vid.bottom
|
|
||||||
anchors.left: vid.left
|
|
||||||
anchors.right: vid.right
|
|
||||||
visible: stateVisible && (videoHoverHandler.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || videoControlTimer.running)
|
|
||||||
|
|
||||||
contentItem: RowLayout {
|
|
||||||
id: controlRow
|
|
||||||
QQC2.ToolButton {
|
|
||||||
id: playButton
|
|
||||||
}
|
|
||||||
QQC2.Slider {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
from: 0
|
|
||||||
to: vid.duration
|
|
||||||
value: vid.position
|
|
||||||
onMoved: vid.seek(value)
|
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
text: Format.formatDuration(vid.position) + "/" + Format.formatDuration(vid.duration)
|
|
||||||
}
|
|
||||||
QQC2.ToolButton {
|
|
||||||
id: volumeButton
|
|
||||||
property var unmuteVolume: vid.volume
|
|
||||||
|
|
||||||
icon.name: vid.volume <= 0 ? "player-volume-muted" : "player-volume"
|
|
||||||
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
||||||
QQC2.ToolTip.timeout: Kirigami.Units.toolTipDelay
|
|
||||||
QQC2.ToolTip.text: i18nc("@action:button", "Volume")
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (vid.volume > 0) {
|
|
||||||
vid.volume = 0;
|
|
||||||
} else {
|
|
||||||
if (unmuteVolume === 0) {
|
|
||||||
vid.volume = 1;
|
|
||||||
} else {
|
|
||||||
vid.volume = unmuteVolume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onHoveredChanged: {
|
|
||||||
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
|
|
||||||
videoControlTimer.restart();
|
|
||||||
volumePopupTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Popup {
|
|
||||||
id: volumePopup
|
|
||||||
y: -height
|
|
||||||
width: volumeButton.width
|
|
||||||
visible: videoControls.stateVisible && (volumeButton.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || volumePopupTimer.running)
|
|
||||||
|
|
||||||
focus: true
|
|
||||||
padding: Kirigami.Units.smallSpacing
|
|
||||||
closePolicy: QQC2.Popup.NoAutoClose
|
|
||||||
|
|
||||||
QQC2.Slider {
|
|
||||||
id: volumeSlider
|
|
||||||
anchors.centerIn: parent
|
|
||||||
implicitHeight: Kirigami.Units.gridUnit * 7
|
|
||||||
orientation: Qt.Vertical
|
|
||||||
padding: 0
|
|
||||||
from: 0
|
|
||||||
to: 1
|
|
||||||
value: vid.volume
|
|
||||||
onMoved: {
|
|
||||||
vid.volume = value;
|
|
||||||
volumeButton.unmuteVolume = value;
|
|
||||||
}
|
|
||||||
onHoveredChanged: {
|
|
||||||
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
|
|
||||||
videoControlTimer.restart();
|
|
||||||
volumePopupTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timer {
|
|
||||||
id: volumePopupTimer
|
|
||||||
interval: 500
|
|
||||||
}
|
|
||||||
HoverHandler {
|
|
||||||
id: volumePopupHoverHandler
|
|
||||||
onHoveredChanged: {
|
|
||||||
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
|
|
||||||
videoControlTimer.restart();
|
|
||||||
volumePopupTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
background: Kirigami.ShadowedRectangle {
|
|
||||||
radius: 4
|
|
||||||
color: Kirigami.Theme.backgroundColor
|
|
||||||
opacity: 0.8
|
|
||||||
|
|
||||||
property color borderColor: Kirigami.Theme.textColor
|
|
||||||
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
shadow.xOffset: 0
|
|
||||||
shadow.yOffset: 4
|
|
||||||
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
|
||||||
shadow.size: 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QQC2.ToolButton {
|
|
||||||
id: maximizeButton
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
|
|
||||||
action: Kirigami.Action {
|
|
||||||
text: i18n("Maximize")
|
|
||||||
icon.name: "view-fullscreen"
|
|
||||||
onTriggered: {
|
|
||||||
root.ListView.view.interactive = false;
|
|
||||||
vid.pause();
|
|
||||||
// We need to make sure the index is that of the MediaMessageFilterModel.
|
|
||||||
if (root.ListView.view.model instanceof MessageFilterModel) {
|
|
||||||
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.index));
|
|
||||||
} else {
|
|
||||||
RoomManager.maximizeMedia(root.index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
background: Kirigami.ShadowedRectangle {
|
|
||||||
radius: 4
|
|
||||||
color: Kirigami.Theme.backgroundColor
|
|
||||||
opacity: 0.8
|
|
||||||
|
|
||||||
property color borderColor: Kirigami.Theme.textColor
|
|
||||||
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
shadow.xOffset: 0
|
|
||||||
shadow.yOffset: 4
|
|
||||||
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
|
||||||
shadow.size: 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: videoControlTimer
|
|
||||||
interval: 1000
|
|
||||||
}
|
|
||||||
HoverHandler {
|
|
||||||
id: videoHoverHandler
|
|
||||||
onHoveredChanged: {
|
|
||||||
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
|
|
||||||
videoControlTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TapHandler {
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
|
|
||||||
onTapped: if (root.progressInfo.completed) {
|
|
||||||
if (vid.playbackState == MediaPlayer.PlayingState) {
|
|
||||||
vid.pause();
|
|
||||||
} else {
|
|
||||||
vid.play();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
root.downloadAndPlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaSizeHelper {
|
|
||||||
id: mediaSizeHelper
|
|
||||||
contentMaxWidth: root.contentMaxWidth
|
|
||||||
mediaWidth: root.mediaInfo.width
|
|
||||||
mediaHeight: root.mediaInfo.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadAndPlay() {
|
|
||||||
if (vid.downloaded) {
|
|
||||||
playSavedFile();
|
|
||||||
} else {
|
|
||||||
playOnFinished = true;
|
|
||||||
root.room.downloadFile(root.eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.room.fileNameToDownload(root.eventId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function playSavedFile() {
|
|
||||||
vid.stop();
|
|
||||||
vid.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,8 @@
|
|||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
|
|
||||||
#include "chatbarcache.h"
|
#include "chatbarcache.h"
|
||||||
#include "enums/delegatetype.h"
|
#include "eventhandler.h"
|
||||||
|
#include "messagecomponenttype.h"
|
||||||
#include "models/timelinemodel.h"
|
#include "models/timelinemodel.h"
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
#include "neochatconnection.h"
|
#include "neochatconnection.h"
|
||||||
@@ -127,22 +128,27 @@ void RoomManager::viewEventSource(const QString &eventId)
|
|||||||
Q_EMIT showEventSource(eventId);
|
Q_EMIT showEventSource(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RoomManager::viewEventMenu(const QString &eventId,
|
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText)
|
||||||
const QVariantMap &author,
|
|
||||||
DelegateType::Type delegateType,
|
|
||||||
const QString &plainText,
|
|
||||||
const QString &htmlText,
|
|
||||||
const QString &selectedText,
|
|
||||||
const QString &mimeType,
|
|
||||||
const FileTransferInfo &progressInfo)
|
|
||||||
{
|
{
|
||||||
if (delegateType == DelegateType::Image || delegateType == DelegateType::Video || delegateType == DelegateType::Audio
|
const auto &event = **room->findInTimeline(eventId);
|
||||||
|| delegateType == DelegateType::File) {
|
const auto eventHandler = EventHandler(room, &event);
|
||||||
Q_EMIT showFileMenu(eventId, author, delegateType, plainText, mimeType, progressInfo);
|
|
||||||
|
if (eventHandler.getMediaInfo().contains("mimeType"_ls)) {
|
||||||
|
Q_EMIT showFileMenu(eventId,
|
||||||
|
eventHandler.getAuthor(),
|
||||||
|
eventHandler.messageComponentType(),
|
||||||
|
eventHandler.getPlainBody(),
|
||||||
|
eventHandler.getMediaInfo()["mimeType"_ls].toString(),
|
||||||
|
room->fileTransferInfo(eventId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_EMIT showMessageMenu(eventId, author, delegateType, plainText, htmlText, selectedText);
|
Q_EMIT showMessageMenu(eventId,
|
||||||
|
eventHandler.getAuthor(),
|
||||||
|
eventHandler.messageComponentType(),
|
||||||
|
eventHandler.getPlainBody(),
|
||||||
|
eventHandler.getRichBody(),
|
||||||
|
selectedText);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RoomManager::hasOpenRoom() const
|
bool RoomManager::hasOpenRoom() const
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
#include <KConfigGroup>
|
#include <KConfigGroup>
|
||||||
|
|
||||||
#include "chatdocumenthandler.h"
|
#include "chatdocumenthandler.h"
|
||||||
#include "enums/delegatetype.h"
|
#include "enums/messagecomponenttype.h"
|
||||||
|
#include "eventhandler.h"
|
||||||
#include "models/mediamessagefiltermodel.h"
|
#include "models/mediamessagefiltermodel.h"
|
||||||
#include "models/messagefiltermodel.h"
|
#include "models/messagefiltermodel.h"
|
||||||
#include "models/timelinemodel.h"
|
#include "models/timelinemodel.h"
|
||||||
@@ -182,16 +183,9 @@ public:
|
|||||||
Q_INVOKABLE void viewEventSource(const QString &eventId);
|
Q_INVOKABLE void viewEventSource(const QString &eventId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Show a conterxt menu for the given event.
|
* @brief Show a context menu for the given event.
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void viewEventMenu(const QString &eventId,
|
Q_INVOKABLE void viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText = {});
|
||||||
const QVariantMap &author,
|
|
||||||
DelegateType::Type delegateType,
|
|
||||||
const QString &plainText,
|
|
||||||
const QString &htmlText = {},
|
|
||||||
const QString &selectedText = {},
|
|
||||||
const QString &mimeType = {},
|
|
||||||
const FileTransferInfo &progressInfo = {});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Call this when the current used connection is dropped.
|
* @brief Call this when the current used connection is dropped.
|
||||||
@@ -299,7 +293,7 @@ Q_SIGNALS:
|
|||||||
*/
|
*/
|
||||||
void showMessageMenu(const QString &eventId,
|
void showMessageMenu(const QString &eventId,
|
||||||
const QVariantMap &author,
|
const QVariantMap &author,
|
||||||
DelegateType::Type delegateType,
|
MessageComponentType::Type messageComponentType,
|
||||||
const QString &plainText,
|
const QString &plainText,
|
||||||
const QString &htmlText,
|
const QString &htmlText,
|
||||||
const QString &selectedText);
|
const QString &selectedText);
|
||||||
@@ -309,7 +303,7 @@ Q_SIGNALS:
|
|||||||
*/
|
*/
|
||||||
void showFileMenu(const QString &eventId,
|
void showFileMenu(const QString &eventId,
|
||||||
const QVariantMap &author,
|
const QVariantMap &author,
|
||||||
DelegateType::Type delegateType,
|
MessageComponentType::Type messageComponentType,
|
||||||
const QString &plainText,
|
const QString &plainText,
|
||||||
const QString &mimeType,
|
const QString &mimeType,
|
||||||
const FileTransferInfo &progressInfo);
|
const FileTransferInfo &progressInfo);
|
||||||
|
|||||||
Reference in New Issue
Block a user