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/syncdata.h>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatroom.h"
|
||||
@@ -37,9 +36,6 @@ private Q_SLOTS:
|
||||
|
||||
void eventId();
|
||||
void nullEventId();
|
||||
void delegateType_data();
|
||||
void delegateType();
|
||||
void nullDelegateType();
|
||||
void author();
|
||||
void nullAuthor();
|
||||
void authorDisplayName();
|
||||
@@ -67,8 +63,6 @@ private Q_SLOTS:
|
||||
void nullHasReply();
|
||||
void replyId();
|
||||
void nullReplyId();
|
||||
void replyDelegateType();
|
||||
void nullReplyDelegateType();
|
||||
void replyAuthor();
|
||||
void nullReplyAuthor();
|
||||
void replyBody();
|
||||
@@ -102,35 +96,6 @@ void EventHandlerTest::nullEventId()
|
||||
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()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
@@ -409,25 +374,6 @@ void EventHandlerTest::nullReplyId()
|
||||
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()
|
||||
{
|
||||
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), 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"));
|
||||
|
||||
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/serveraclevent.h
|
||||
events/widgetevent.h
|
||||
enums/messagecomponenttype.h
|
||||
models/messagecontentmodel.cpp
|
||||
models/messagecontentmodel.h
|
||||
)
|
||||
|
||||
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/ReplyComponent.qml
|
||||
qml/StateDelegate.qml
|
||||
qml/RichLabel.qml
|
||||
qml/MessageDelegate.qml
|
||||
qml/Bubble.qml
|
||||
qml/SectionDelegate.qml
|
||||
qml/VideoDelegate.qml
|
||||
qml/ReactionDelegate.qml
|
||||
qml/LinkPreviewDelegate.qml
|
||||
qml/AudioDelegate.qml
|
||||
qml/FileDelegate.qml
|
||||
qml/ImageDelegate.qml
|
||||
qml/EncryptedDelegate.qml
|
||||
qml/EventDelegate.qml
|
||||
qml/TextDelegate.qml
|
||||
qml/ReadMarkerDelegate.qml
|
||||
qml/PollDelegate.qml
|
||||
qml/MimeComponent.qml
|
||||
qml/StateComponent.qml
|
||||
qml/MessageEditComponent.qml
|
||||
@@ -276,14 +270,12 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
qml/EmojiDelegate.qml
|
||||
qml/EmojiGrid.qml
|
||||
qml/RoomSearchPage.qml
|
||||
qml/LocationDelegate.qml
|
||||
qml/LocationChooser.qml
|
||||
qml/TimelineView.qml
|
||||
qml/InvitationView.qml
|
||||
qml/AvatarTabButton.qml
|
||||
qml/SpaceDrawer.qml
|
||||
qml/OsmLocationPlugin.qml
|
||||
qml/LiveLocationDelegate.qml
|
||||
qml/FullScreenMap.qml
|
||||
qml/LocationsPage.qml
|
||||
qml/LocationMapItem.qml
|
||||
@@ -310,6 +302,18 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
qml/ServerComboBox.qml
|
||||
qml/UserSearchPage.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
|
||||
qml/confetti.png
|
||||
qml/glowdot.png
|
||||
|
||||
@@ -43,14 +43,14 @@ void ChatBarCache::setReplyId(const QString &replyId)
|
||||
if (m_relationType == Reply && m_relationId == replyId) {
|
||||
return;
|
||||
}
|
||||
m_relationId = replyId;
|
||||
const auto oldEventId = std::exchange(m_relationId, replyId);
|
||||
if (m_relationId.isEmpty()) {
|
||||
m_relationType = None;
|
||||
} else {
|
||||
m_relationType = Reply;
|
||||
}
|
||||
m_attachmentPath = QString();
|
||||
Q_EMIT relationIdChanged();
|
||||
Q_EMIT relationIdChanged(oldEventId, m_relationId);
|
||||
Q_EMIT attachmentPathChanged();
|
||||
}
|
||||
|
||||
@@ -72,14 +72,14 @@ void ChatBarCache::setEditId(const QString &editId)
|
||||
if (m_relationType == Edit && m_relationId == editId) {
|
||||
return;
|
||||
}
|
||||
m_relationId = editId;
|
||||
const auto oldEventId = std::exchange(m_relationId, editId);
|
||||
if (m_relationId.isEmpty()) {
|
||||
m_relationType = None;
|
||||
} else {
|
||||
m_relationType = Edit;
|
||||
}
|
||||
m_attachmentPath = QString();
|
||||
Q_EMIT relationIdChanged();
|
||||
Q_EMIT relationIdChanged(oldEventId, m_relationId);
|
||||
Q_EMIT attachmentPathChanged();
|
||||
}
|
||||
|
||||
@@ -153,9 +153,9 @@ void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
|
||||
}
|
||||
m_attachmentPath = attachmentPath;
|
||||
m_relationType = None;
|
||||
m_relationId = QString();
|
||||
const auto oldEventId = std::exchange(m_relationId, QString());
|
||||
Q_EMIT attachmentPathChanged();
|
||||
Q_EMIT relationIdChanged();
|
||||
Q_EMIT relationIdChanged(oldEventId, m_relationId);
|
||||
}
|
||||
|
||||
QList<Mention> *ChatBarCache::mentions()
|
||||
|
||||
@@ -186,7 +186,7 @@ public:
|
||||
|
||||
Q_SIGNALS:
|
||||
void textChanged();
|
||||
void relationIdChanged();
|
||||
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
|
||||
void threadIdChanged();
|
||||
void attachmentPathChanged();
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
#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 DelegateType
|
||||
*
|
||||
@@ -26,23 +33,34 @@ public:
|
||||
* similar to the spec it is not the same.
|
||||
*/
|
||||
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. */
|
||||
Sticker, /**< A message that is a sticker. */
|
||||
State, /**< A state event in the room. */
|
||||
Encrypted, /**< An encrypted message that cannot be decrypted. */
|
||||
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. */
|
||||
TimelineEnd, /**< A delegate to inform that all messages are loaded. */
|
||||
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)
|
||||
{
|
||||
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/roomcanonicalaliasevent.h>
|
||||
#include <Quotient/events/roommemberevent.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <Quotient/events/roompowerlevelsevent.h>
|
||||
#include <Quotient/events/simplestateevents.h>
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
|
||||
#include "delegatetype.h"
|
||||
#include "eventhandler_logging.h"
|
||||
#include "events/locationbeaconevent.h"
|
||||
#include "events/pollevent.h"
|
||||
#include "events/serveraclevent.h"
|
||||
#include "events/widgetevent.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "messagecomponenttype.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
@@ -50,62 +51,14 @@ QString EventHandler::getId() const
|
||||
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
|
||||
}
|
||||
|
||||
DelegateType::Type EventHandler::getDelegateTypeForEvent(const Quotient::RoomEvent *event) 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
|
||||
MessageComponentType::Type EventHandler::messageComponentType() const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getDelegateType called with m_event set to nullptr.";
|
||||
return DelegateType::Other;
|
||||
qCWarning(EventHandling) << "messageComponentType called with m_event set to nullptr.";
|
||||
return MessageComponentType::Other;
|
||||
}
|
||||
|
||||
return getDelegateTypeForEvent(m_event);
|
||||
return MessageComponentType::typeForEvent(*m_event);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
DelegateType::Type EventHandler::getReplyDelegateType() const
|
||||
MessageComponentType::Type EventHandler::replyMessageComponentType() const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyDelegateType called with m_room set to nullptr.";
|
||||
return DelegateType::Other;
|
||||
qCWarning(EventHandling) << "replyMessageComponentType called with m_room set to nullptr.";
|
||||
return MessageComponentType::Other;
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyDelegateType called with m_event set to nullptr.";
|
||||
return DelegateType::Other;
|
||||
qCWarning(EventHandling) << "replyMessageComponentType called with m_event set to nullptr.";
|
||||
return MessageComponentType::Other;
|
||||
}
|
||||
|
||||
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||
if (replyEvent == nullptr) {
|
||||
return DelegateType::Other;
|
||||
return MessageComponentType::Other;
|
||||
}
|
||||
return getDelegateTypeForEvent(replyEvent);
|
||||
return MessageComponentType::typeForEvent(*replyEvent);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getReplyAuthor() const
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include <Quotient/events/roomevent.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "enums/messagecomponenttype.h"
|
||||
|
||||
class LinkPreviewer;
|
||||
class NeoChatRoom;
|
||||
@@ -44,13 +44,9 @@ public:
|
||||
QString getId() const;
|
||||
|
||||
/**
|
||||
* @brief Return the DelegateType of the event.
|
||||
*
|
||||
* @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.
|
||||
* @brief The MessageComponentType to use to visualise the main event content.
|
||||
*/
|
||||
DelegateType::Type getDelegateType() const;
|
||||
MessageComponentType::Type messageComponentType() const;
|
||||
|
||||
/**
|
||||
* @brief Get the author of the event in context of the room.
|
||||
@@ -224,13 +220,9 @@ public:
|
||||
QString getReplyId() const;
|
||||
|
||||
/**
|
||||
* @brief Return the DelegateType of the event replied to.
|
||||
*
|
||||
* @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.
|
||||
* @brief The MessageComponentType to use to visualise the reply content.
|
||||
*/
|
||||
DelegateType::Type getReplyDelegateType() const;
|
||||
MessageComponentType::Type replyMessageComponentType() const;
|
||||
|
||||
/**
|
||||
* @brief Get the author of the event replied to in context of the room.
|
||||
@@ -386,8 +378,6 @@ private:
|
||||
|
||||
KFormat m_format;
|
||||
|
||||
DelegateType::Type getDelegateTypeForEvent(const Quotient::RoomEvent *event) 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;
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event)
|
||||
: QObject(nullptr)
|
||||
LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_currentRoom(room)
|
||||
, m_event(event)
|
||||
, m_loaded(false)
|
||||
|
||||
@@ -60,7 +60,7 @@ class LinkPreviewer : public QObject
|
||||
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
|
||||
|
||||
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]] bool loaded() const;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
#include "mediamessagefiltermodel.h"
|
||||
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <Quotient/room.h>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "messagefiltermodel.h"
|
||||
|
||||
@@ -20,8 +20,8 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
|
||||
{
|
||||
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
|
||||
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image
|
||||
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
|
||||
if (index.data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))
|
||||
|| index.data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("video"))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -30,9 +30,9 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
|
||||
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
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();
|
||||
} 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>();
|
||||
|
||||
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();
|
||||
}
|
||||
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;
|
||||
} else {
|
||||
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
|
||||
|
||||
#include "messageeventmodel.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "messageeventmodel_logging.h"
|
||||
|
||||
#include "neochatconfig.h"
|
||||
@@ -10,6 +9,7 @@
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/csapi/rooms.h>
|
||||
#include <Quotient/events/redactionevent.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
#include <Quotient/user.h>
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "enums/delegatetype.h"
|
||||
#include "eventhandler.h"
|
||||
#include "events/pollevent.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "messagecontentmodel.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
@@ -31,7 +33,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
|
||||
roles[DelegateTypeRole] = "delegateType";
|
||||
roles[PlainText] = "plainText";
|
||||
roles[EventIdRole] = "eventId";
|
||||
roles[TimeRole] = "time";
|
||||
roles[TimeStringRole] = "timeString";
|
||||
@@ -40,15 +41,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[HighlightRole] = "isHighlighted";
|
||||
roles[SpecialMarksRole] = "marks";
|
||||
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[ThreadRootRole] = "threadRoot";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
@@ -64,10 +56,8 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[IsRedactedRole] = "isRedacted";
|
||||
roles[GenericDisplayRole] = "genericDisplay";
|
||||
roles[IsPendingRole] = "isPending";
|
||||
roles[LatitudeRole] = "latitude";
|
||||
roles[LongitudeRole] = "longitude";
|
||||
roles[AssetRole] = "asset";
|
||||
roles[PollHandlerRole] = "pollHandler";
|
||||
roles[ContentModelRole] = "contentModel";
|
||||
roles[MediaInfoRole] = "mediaInfo";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -96,7 +86,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
beginResetModel();
|
||||
if (m_currentRoom) {
|
||||
m_currentRoom->disconnect(this);
|
||||
m_linkPreviewers.clear();
|
||||
m_reactionModels.clear();
|
||||
}
|
||||
|
||||
@@ -119,14 +108,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
room->getPreviousContent(50);
|
||||
}
|
||||
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) {
|
||||
for (auto &&event : events) {
|
||||
@@ -238,7 +219,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
moveReadMarker(toEventId);
|
||||
});
|
||||
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
||||
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
|
||||
const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
|
||||
if (message != nullptr) {
|
||||
createEventObjects(message);
|
||||
@@ -265,10 +245,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
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] {
|
||||
beginResetModel();
|
||||
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
|
||||
{
|
||||
if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
|
||||
@@ -492,16 +466,24 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
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) {
|
||||
return eventHandler.getGenericBody();
|
||||
}
|
||||
|
||||
if (role == PlainText) {
|
||||
return eventHandler.getPlainBody();
|
||||
}
|
||||
|
||||
if (role == DelegateTypeRole) {
|
||||
return eventHandler.getDelegateType();
|
||||
return DelegateType::typeForEvent(evt);
|
||||
}
|
||||
|
||||
if (role == AuthorRole) {
|
||||
@@ -559,46 +541,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
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) {
|
||||
return eventHandler.isThreaded();
|
||||
}
|
||||
@@ -642,18 +584,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role == LatitudeRole) {
|
||||
return eventHandler.getLatitude();
|
||||
}
|
||||
|
||||
if (role == LongitudeRole) {
|
||||
return eventHandler.getLongitude();
|
||||
}
|
||||
|
||||
if (role == AssetRole) {
|
||||
return eventHandler.getLocationAssetType();
|
||||
}
|
||||
|
||||
if (role == ReadMarkersRole) {
|
||||
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());
|
||||
}
|
||||
|
||||
if (role == PollHandlerRole) {
|
||||
return QVariant::fromValue<PollHandler *>(m_currentRoom->poll(evt.id()));
|
||||
if (role == MediaInfoRole) {
|
||||
return eventHandler.getMediaInfo();
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -724,16 +654,6 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
|
||||
{
|
||||
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
|
||||
// handle adding and removing whole models here.
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
@@ -761,7 +681,7 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
|
||||
bool MessageEventModel::event(QEvent *event)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ public:
|
||||
*/
|
||||
enum EventRoles {
|
||||
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. */
|
||||
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). */
|
||||
@@ -50,18 +49,9 @@ public:
|
||||
SpecialMarksRole, /**< Whether the event is hidden or not. */
|
||||
ProgressInfoRole, /**< Progress info when downloading files. */
|
||||
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. */
|
||||
|
||||
IsReplyRole, /**< Is the message a reply to another 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. */
|
||||
ContentModelRole, /**< The MessageContentModel for the event. */
|
||||
|
||||
IsThreadedRole,
|
||||
ThreadRootRole,
|
||||
@@ -80,10 +70,6 @@ public:
|
||||
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
|
||||
IsRedactedRole, /**< Whether an event has been deleted. */
|
||||
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
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
@@ -135,7 +121,6 @@ private:
|
||||
bool movingEvent = false;
|
||||
KFormat m_format;
|
||||
|
||||
QMap<QString, QSharedPointer<LinkPreviewer>> m_linkPreviewers;
|
||||
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||
|
||||
[[nodiscard]] int timelineBaseIndex() const;
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
#include "searchmodel.h"
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "eventhandler.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "models/messagecontentmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
@@ -82,8 +83,6 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
EventHandler eventHandler(m_room, &event);
|
||||
|
||||
switch (role) {
|
||||
case DisplayRole:
|
||||
return eventHandler.getRichBody();
|
||||
case ShowAuthorRole:
|
||||
return true;
|
||||
case AuthorRole:
|
||||
@@ -103,22 +102,8 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
return false;
|
||||
case ShowReadMarkersRole:
|
||||
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:
|
||||
return false;
|
||||
case ShowLinkPreviewRole:
|
||||
return false;
|
||||
case HighlightRole:
|
||||
return eventHandler.isHighlighted();
|
||||
case EventIdRole:
|
||||
@@ -127,6 +112,17 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
return eventHandler.isThreaded();
|
||||
case ThreadRootRole:
|
||||
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;
|
||||
}
|
||||
@@ -144,7 +140,6 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{DelegateTypeRole, "delegateType"},
|
||||
{DisplayRole, "display"},
|
||||
{AuthorRole, "author"},
|
||||
{ShowSectionRole, "showSection"},
|
||||
{SectionRole, "section"},
|
||||
@@ -155,25 +150,15 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
||||
{ExcessReadMarkersRole, "excessReadMarkers"},
|
||||
{HighlightRole, "isHighlighted"},
|
||||
{ReadMarkersString, "readMarkersString"},
|
||||
{PlainTextRole, "plainText"},
|
||||
{VerifiedRole, "verified"},
|
||||
{ProgressInfoRole, "progressInfo"},
|
||||
{ShowReactionsRole, "showReactions"},
|
||||
{IsReplyRole, "isReply"},
|
||||
{ReplyAuthorRole, "replyAuthor"},
|
||||
{ReplyIdRole, "replyId"},
|
||||
{ReplyDelegateTypeRole, "replyDelegateType"},
|
||||
{ReplyDisplayRole, "replyDisplay"},
|
||||
{ReplyMediaInfoRole, "replyMediaInfo"},
|
||||
{ReactionRole, "reaction"},
|
||||
{ReadMarkersRole, "readMarkers"},
|
||||
{IsPendingRole, "isPending"},
|
||||
{ShowReadMarkersRole, "showReadMarkers"},
|
||||
{MimeTypeRole, "mimeType"},
|
||||
{ShowLinkPreviewRole, "showLinkPreview"},
|
||||
{LinkPreviewRole, "linkPreview"},
|
||||
{IsThreadedRole, "isThreaded"},
|
||||
{ThreadRootRole, "threadRoot"},
|
||||
{ContentModelRole, "contentModel"},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -189,19 +174,6 @@ void SearchModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
m_room = room;
|
||||
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
|
||||
|
||||
@@ -51,8 +51,7 @@ public:
|
||||
* since the same delegates are used.
|
||||
*/
|
||||
enum Roles {
|
||||
DisplayRole = Qt::DisplayRole,
|
||||
DelegateTypeRole,
|
||||
DelegateTypeRole = Qt::DisplayRole + 1,
|
||||
ShowAuthorRole,
|
||||
AuthorRole,
|
||||
ShowSectionRole,
|
||||
@@ -63,25 +62,15 @@ public:
|
||||
ExcessReadMarkersRole,
|
||||
HighlightRole,
|
||||
ReadMarkersString,
|
||||
PlainTextRole,
|
||||
VerifiedRole,
|
||||
ProgressInfoRole,
|
||||
ShowReactionsRole,
|
||||
IsReplyRole,
|
||||
ReplyAuthorRole,
|
||||
ReplyIdRole,
|
||||
ReplyDelegateTypeRole,
|
||||
ReplyDisplayRole,
|
||||
ReplyMediaInfoRole,
|
||||
ReactionRole,
|
||||
ReadMarkersRole,
|
||||
IsPendingRole,
|
||||
ShowReadMarkersRole,
|
||||
MimeTypeRole,
|
||||
ShowLinkPreviewRole,
|
||||
LinkPreviewRole,
|
||||
IsThreadedRole,
|
||||
ThreadRootRole,
|
||||
ContentModelRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
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 {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
@@ -61,70 +66,28 @@ QQC2.Control {
|
||||
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.
|
||||
*/
|
||||
property bool isReply: false
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the reply event.
|
||||
*/
|
||||
required property var replyId
|
||||
|
||||
/**
|
||||
* @brief The reply author.
|
||||
* @brief The ActionsHandler object to use.
|
||||
*
|
||||
* 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
|
||||
* This is expected to have the correct room set otherwise messages will be sent
|
||||
* to the wrong room.
|
||||
*/
|
||||
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
|
||||
property ActionsHandler actionsHandler
|
||||
|
||||
/**
|
||||
* @brief Whether the bubble background should be shown.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -135,11 +98,26 @@ QQC2.Control {
|
||||
*/
|
||||
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 {
|
||||
id: contentColumn
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
implicitHeight: Math.max(nameButton.implicitHeight, timeLabel.implicitHeight)
|
||||
visible: root.showAuthor
|
||||
QQC2.AbstractButton {
|
||||
id: nameButton
|
||||
Layout.fillWidth: true
|
||||
contentItem: QQC2.Label {
|
||||
text: root.author.displayName
|
||||
@@ -152,6 +130,7 @@ QQC2.Control {
|
||||
onClicked: RoomManager.resolveResource(root.author.id, "mention")
|
||||
}
|
||||
QQC2.Label {
|
||||
id: timeLabel
|
||||
text: root.timeString
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
@@ -164,35 +143,19 @@ QQC2.Control {
|
||||
}
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: replyLoader
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
Repeater {
|
||||
id: contentRepeater
|
||||
model: root.contentModel
|
||||
delegate: MessageComponentChooser {
|
||||
room: root.room
|
||||
actionsHandler: root.actionsHandler
|
||||
timeline: root.timeline
|
||||
maxContentWidth: root.maxContentWidth
|
||||
|
||||
active: root.isReply && root.replyDelegateType !== DelegateType.Other
|
||||
visible: active
|
||||
|
||||
sourceComponent: ReplyComponent {
|
||||
author: root.replyAuthor
|
||||
type: root.replyDelegateType
|
||||
display: root.replyDisplay
|
||||
mediaInfo: root.replyMediaInfo
|
||||
contentMaxWidth: root.maxContentWidth
|
||||
onReplyClicked: (eventId) => {root.replyClicked(eventId)}
|
||||
onSelectedTextChanged: (selectedText) => {root.selectedTextChanged(selectedText);}
|
||||
onShowMessageMenu: root.showMessageMenu()
|
||||
}
|
||||
|
||||
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: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
@@ -23,65 +24,9 @@ DelegateChooser {
|
||||
delegate: StateDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Emote
|
||||
delegate: TextDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Message
|
||||
delegate: TextDelegate {
|
||||
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 {
|
||||
delegate: MessageDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
@@ -91,27 +36,6 @@ DelegateChooser {
|
||||
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 {
|
||||
roleValue: DelegateType.Loading
|
||||
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
|
||||
|
||||
/**
|
||||
* @brief The delegate type of the message.
|
||||
* @brief The model to visualise the content of the message.
|
||||
*/
|
||||
required property int delegateType
|
||||
|
||||
/**
|
||||
* @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
|
||||
required property MessageContentModel contentModel
|
||||
|
||||
/**
|
||||
* @brief The date of the event as a string.
|
||||
@@ -142,65 +132,10 @@ TimelineDelegate {
|
||||
*/
|
||||
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 string threadRoot
|
||||
|
||||
/**
|
||||
* @brief Whether this message is replying to another.
|
||||
*/
|
||||
required property bool isReply
|
||||
|
||||
/**
|
||||
* @brief Whether this message has a local user mention.
|
||||
*/
|
||||
@@ -211,13 +146,6 @@ TimelineDelegate {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -249,11 +177,6 @@ TimelineDelegate {
|
||||
*/
|
||||
readonly property alias hovered: bubble.hovered
|
||||
|
||||
/**
|
||||
* @brief Open the context menu for the message.
|
||||
*/
|
||||
signal openContextMenu
|
||||
|
||||
/**
|
||||
* @brief Open the any message media externally.
|
||||
*/
|
||||
@@ -268,7 +191,7 @@ TimelineDelegate {
|
||||
/**
|
||||
* @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.
|
||||
@@ -293,6 +216,11 @@ TimelineDelegate {
|
||||
*/
|
||||
property bool isTemporaryHighlighted: false
|
||||
|
||||
/**
|
||||
* @brief The user selected text.
|
||||
*/
|
||||
property string selectedText: ""
|
||||
|
||||
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) {
|
||||
temporaryHighlightTimer.start();
|
||||
}
|
||||
@@ -329,12 +257,6 @@ TimelineDelegate {
|
||||
|
||||
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
|
||||
onHoveredChanged: {
|
||||
if (hovered && !Kirigami.Settings.isMobile) {
|
||||
@@ -395,23 +317,24 @@ TimelineDelegate {
|
||||
}
|
||||
]
|
||||
|
||||
room: root.room
|
||||
|
||||
author: root.author
|
||||
showAuthor: root.showAuthor || root.alwaysShowAuthor
|
||||
time: root.time
|
||||
timeString: root.timeString
|
||||
|
||||
showHighlight: root.showHighlight
|
||||
contentModel: root.contentModel
|
||||
actionsHandler: root.ListView.view?.actionsHandler ?? null
|
||||
timeline: root.ListView.view
|
||||
|
||||
isReply: root.isReply
|
||||
replyId: root.replyId
|
||||
replyAuthor: root.replyAuthor
|
||||
replyDelegateType: root.replyDelegateType
|
||||
replyDisplay: root.replyDisplay
|
||||
replyMediaInfo: root.replyMediaInfo
|
||||
showHighlight: root.showHighlight
|
||||
|
||||
onReplyClicked: eventId => {
|
||||
root.replyClicked(eventId);
|
||||
}
|
||||
onSelectedTextChanged: (selectedText) => {root.selectedText = selectedText;}
|
||||
onShowMessageMenu: _private.showMessageMenu()
|
||||
|
||||
showBackground: root.cardBackground && !Config.compactLayout
|
||||
}
|
||||
@@ -424,12 +347,12 @@ TimelineDelegate {
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: root.openContextMenu()
|
||||
onTapped: _private.showMessageMenu()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: root.openContextMenu()
|
||||
onLongPressed: _private.showMessageMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,5 +405,9 @@ TimelineDelegate {
|
||||
* @brief Whether local user messages should be aligned right.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
required property int delegateType
|
||||
required property int messageComponentType
|
||||
|
||||
/**
|
||||
* @brief The display text of the message as plain text.
|
||||
@@ -96,7 +96,7 @@ Loader {
|
||||
currentRoom.editCache.editId = eventId;
|
||||
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 {
|
||||
text: i18n("Reply")
|
||||
|
||||
@@ -9,13 +9,19 @@ import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show an edit text field for a text message being edited.
|
||||
*/
|
||||
QQC2.TextArea {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
onRoomChanged: {
|
||||
_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
|
||||
|
||||
property string messageId
|
||||
|
||||
property var minimumHeight: editButtons.height + topPadding + bottomPadding
|
||||
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
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
|
||||
@@ -7,6 +7,9 @@ import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
/**
|
||||
* @brief A component to show media based upon its mime type.
|
||||
*/
|
||||
RowLayout {
|
||||
property alias mimeIconSource: icon.source
|
||||
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 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)
|
||||
|
||||
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: {
|
||||
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.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
@@ -23,6 +24,16 @@ import org.kde.neochat
|
||||
RowLayout {
|
||||
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.
|
||||
*
|
||||
@@ -39,17 +50,12 @@ RowLayout {
|
||||
*
|
||||
* @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
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
required property string replyDisplay
|
||||
|
||||
/**
|
||||
* @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).
|
||||
* - 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.
|
||||
*/
|
||||
signal replyClicked
|
||||
signal replyClicked(string eventID)
|
||||
|
||||
implicitHeight: contentColumn.implicitHeight
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Rectangle {
|
||||
@@ -82,12 +92,17 @@ RowLayout {
|
||||
Layout.fillHeight: true
|
||||
|
||||
implicitWidth: Kirigami.Units.smallSpacing
|
||||
color: root.author.color
|
||||
color: root.replyAuthor.color
|
||||
}
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
implicitHeight: headerRow.implicitHeight + (root.replyComponentType != MessageComponentType.Other ? contentRepeater.itemAt(0).implicitHeight + spacing : 0)
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
implicitHeight: Math.max(replyAvatar.implicitHeight, replyName.implicitHeight)
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
@@ -96,42 +111,87 @@ RowLayout {
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
|
||||
source: root.author.avatarSource
|
||||
name: root.author.displayName
|
||||
color: root.author.color
|
||||
source: root.replyAuthor.avatarSource
|
||||
name: root.replyAuthor.displayName
|
||||
color: root.replyAuthor.color
|
||||
}
|
||||
QQC2.Label {
|
||||
id: replyName
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: root.author.color
|
||||
text: root.author.displayName
|
||||
color: root.replyAuthor.color
|
||||
text: root.replyAuthor.displayName
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: loader
|
||||
Repeater {
|
||||
id: contentRepeater
|
||||
model: [root.replyComponentType]
|
||||
delegate: DelegateChooser {
|
||||
role: "modelData"
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: loader.item && (root.type == DelegateType.Image || root.type == DelegateType.Sticker) ? loader.item.height : loader.item.implicitHeight
|
||||
Layout.columnSpan: 2
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Text
|
||||
delegate: TextComponent {
|
||||
display: root.replyDisplay
|
||||
maxContentWidth: _private.availableContentWidth
|
||||
|
||||
sourceComponent: {
|
||||
switch (root.type) {
|
||||
case DelegateType.Image:
|
||||
case DelegateType.Sticker:
|
||||
return imageComponent;
|
||||
case DelegateType.Message:
|
||||
case DelegateType.Notice:
|
||||
return textComponent;
|
||||
case DelegateType.File:
|
||||
case DelegateType.Video:
|
||||
case DelegateType.Audio:
|
||||
return mimeComponent;
|
||||
case DelegateType.Encrypted:
|
||||
return encryptedComponent;
|
||||
default:
|
||||
return textComponent;
|
||||
HoverHandler {
|
||||
enabled: !hoveredLink
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
TapHandler {
|
||||
enabled: !hoveredLink
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: root.replyClicked(root.replyEventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Image
|
||||
delegate: Image {
|
||||
id: image
|
||||
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 {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: root.replyClicked()
|
||||
onTapped: root.replyClicked(root.replyEventId)
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComponent
|
||||
RichLabel {
|
||||
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
|
||||
}
|
||||
QtObject {
|
||||
id: _private
|
||||
// The space available for the component after taking away the border
|
||||
readonly property real availableContentWidth: root.maxContentWidth - verticalBorder.implicitWidth - root.spacing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ QQC2.ScrollView {
|
||||
role: "type"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: 0//MediaMessageFilterModel.Image
|
||||
delegate: ImageDelegate {
|
||||
roleValue: MediaMessageFilterModel.Image
|
||||
delegate: MessageDelegate {
|
||||
alwaysShowAuthor: true
|
||||
alwaysMaxWidth: true
|
||||
cardBackground: false
|
||||
@@ -57,8 +57,8 @@ QQC2.ScrollView {
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: 1//MediaMessageFilterModel.Video
|
||||
delegate: VideoDelegate {
|
||||
roleValue: MediaMessageFilterModel.Video
|
||||
delegate: MessageDelegate {
|
||||
alwaysShowAuthor: true
|
||||
alwaysMaxWidth: true
|
||||
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, {
|
||||
selectedText: selectedText,
|
||||
author: author,
|
||||
eventId: eventId,
|
||||
delegateType: delegateType,
|
||||
messageComponentType: messageComponentType,
|
||||
plainText: plainText,
|
||||
htmlText: htmlText
|
||||
});
|
||||
contextMenu.open();
|
||||
}
|
||||
|
||||
function onShowFileMenu(eventId, author, delegateType, plainText, mimeType, progressInfo) {
|
||||
function onShowFileMenu(eventId, author, messageComponentType, plainText, mimeType, progressInfo) {
|
||||
const contextMenu = fileDelegateContextMenu.createObject(root, {
|
||||
author: author,
|
||||
eventId: eventId,
|
||||
delegateType: delegateType,
|
||||
messageComponentType: messageComponentType,
|
||||
plainText: plainText,
|
||||
mimeType: mimeType,
|
||||
progressInfo: progressInfo
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
// 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
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.neochat
|
||||
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 {
|
||||
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.
|
||||
*/
|
||||
property bool isReply
|
||||
property bool isReply: false
|
||||
|
||||
/**
|
||||
* @brief Regex for detecting a message with a single emoji.
|
||||
@@ -31,7 +33,7 @@ TextEdit {
|
||||
/**
|
||||
* @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.
|
||||
@@ -41,9 +43,23 @@ TextEdit {
|
||||
/**
|
||||
* @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
|
||||
|
||||
@@ -91,7 +107,7 @@ a{
|
||||
background: " + Kirigami.Theme.textColor + ";
|
||||
}
|
||||
" : "") + "
|
||||
</style>" + textMessage
|
||||
</style>" + display
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
@@ -106,8 +122,8 @@ a{
|
||||
textFormat: Text.RichText
|
||||
|
||||
onLinkActivated: link => {
|
||||
spoilerRevealed = true;
|
||||
RoomManager.resolveResource(link, "join");
|
||||
spoilerRevealed = true
|
||||
RoomManager.resolveResource(link, "join")
|
||||
}
|
||||
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
|
||||
applicationWindow().hoverLinkIndicator.text = hoveredLink;
|
||||
@@ -116,11 +132,16 @@ a{
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: (parent.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
cursorShape: (root.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: !parent.hoveredLink && !spoilerRevealed
|
||||
enabled: !root.hoveredLink && !spoilerRevealed
|
||||
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 "chatbarcache.h"
|
||||
#include "enums/delegatetype.h"
|
||||
#include "eventhandler.h"
|
||||
#include "messagecomponenttype.h"
|
||||
#include "models/timelinemodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatconnection.h"
|
||||
@@ -127,22 +128,27 @@ void RoomManager::viewEventSource(const QString &eventId)
|
||||
Q_EMIT showEventSource(eventId);
|
||||
}
|
||||
|
||||
void RoomManager::viewEventMenu(const QString &eventId,
|
||||
const QVariantMap &author,
|
||||
DelegateType::Type delegateType,
|
||||
const QString &plainText,
|
||||
const QString &htmlText,
|
||||
const QString &selectedText,
|
||||
const QString &mimeType,
|
||||
const FileTransferInfo &progressInfo)
|
||||
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText)
|
||||
{
|
||||
if (delegateType == DelegateType::Image || delegateType == DelegateType::Video || delegateType == DelegateType::Audio
|
||||
|| delegateType == DelegateType::File) {
|
||||
Q_EMIT showFileMenu(eventId, author, delegateType, plainText, mimeType, progressInfo);
|
||||
const auto &event = **room->findInTimeline(eventId);
|
||||
const auto eventHandler = EventHandler(room, &event);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
#include <KConfigGroup>
|
||||
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "enums/delegatetype.h"
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "eventhandler.h"
|
||||
#include "models/mediamessagefiltermodel.h"
|
||||
#include "models/messagefiltermodel.h"
|
||||
#include "models/timelinemodel.h"
|
||||
@@ -182,16 +183,9 @@ public:
|
||||
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,
|
||||
const QVariantMap &author,
|
||||
DelegateType::Type delegateType,
|
||||
const QString &plainText,
|
||||
const QString &htmlText = {},
|
||||
const QString &selectedText = {},
|
||||
const QString &mimeType = {},
|
||||
const FileTransferInfo &progressInfo = {});
|
||||
Q_INVOKABLE void viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText = {});
|
||||
|
||||
/**
|
||||
* @brief Call this when the current used connection is dropped.
|
||||
@@ -299,7 +293,7 @@ Q_SIGNALS:
|
||||
*/
|
||||
void showMessageMenu(const QString &eventId,
|
||||
const QVariantMap &author,
|
||||
DelegateType::Type delegateType,
|
||||
MessageComponentType::Type messageComponentType,
|
||||
const QString &plainText,
|
||||
const QString &htmlText,
|
||||
const QString &selectedText);
|
||||
@@ -309,7 +303,7 @@ Q_SIGNALS:
|
||||
*/
|
||||
void showFileMenu(const QString &eventId,
|
||||
const QVariantMap &author,
|
||||
DelegateType::Type delegateType,
|
||||
MessageComponentType::Type messageComponentType,
|
||||
const QString &plainText,
|
||||
const QString &mimeType,
|
||||
const FileTransferInfo &progressInfo);
|
||||
|
||||
Reference in New Issue
Block a user