Use MessageContentModel for replies
This allows code and other components to be displayed nicely.
This commit is contained in:
@@ -47,11 +47,11 @@ public:
|
|||||||
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
|
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. */
|
Encrypted, /**< An encrypted message that cannot be decrypted. */
|
||||||
Reply, /**< A component to show a replied-to message. */
|
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. */
|
LinkPreview, /**< A preview of a URL in the message. */
|
||||||
LinkPreviewLoad, /**< A loading dialog for a link preview. */
|
LinkPreviewLoad, /**< A loading dialog for a link preview. */
|
||||||
Edit, /**< A text edit for editing a message. */
|
Edit, /**< A text edit for editing a message. */
|
||||||
Verification, /**< A user verification session start message. */
|
Verification, /**< A user verification session start message. */
|
||||||
|
Loading, /**< The component is loading. */
|
||||||
Other, /**< Anything that cannot be classified as another type. */
|
Other, /**< Anything that cannot be classified as another type. */
|
||||||
};
|
};
|
||||||
Q_ENUM(Type);
|
Q_ENUM(Type);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <Quotient/events/stickerevent.h>
|
#include <Quotient/events/stickerevent.h>
|
||||||
|
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
|
#include <Quotient/qt_connection_util.h>
|
||||||
|
|
||||||
#ifndef Q_OS_ANDROID
|
#ifndef Q_OS_ANDROID
|
||||||
#include <KSyntaxHighlighting/Definition>
|
#include <KSyntaxHighlighting/Definition>
|
||||||
@@ -29,95 +30,124 @@
|
|||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoChatRoom *room)
|
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply)
|
||||||
: QAbstractListModel(nullptr)
|
: QAbstractListModel(nullptr)
|
||||||
, m_room(room)
|
, m_room(room)
|
||||||
|
, m_eventId(event != nullptr ? event->id() : QString())
|
||||||
, m_event(event)
|
, m_event(event)
|
||||||
|
, m_isReply(isReply)
|
||||||
{
|
{
|
||||||
if (m_room != nullptr) {
|
initializeModel();
|
||||||
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_room != 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].type = MessageComponentType::Reply;
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
|
|
||||||
if (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 && 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 && eventId == m_event->id()) {
|
|
||||||
updateComponents();
|
|
||||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
|
||||||
|
|
||||||
QString mxcUrl;
|
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply)
|
||||||
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
|
: QAbstractListModel(nullptr)
|
||||||
if (event->hasFileContent()) {
|
, m_room(room)
|
||||||
mxcUrl = event->content()->fileInfo()->url().toString();
|
, m_eventId(eventId)
|
||||||
}
|
, m_isReply(isReply)
|
||||||
} else if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
|
{
|
||||||
mxcUrl = event->image().fileInfo()->url().toString();
|
initializeModel();
|
||||||
}
|
}
|
||||||
if (mxcUrl.isEmpty()) {
|
|
||||||
return;
|
void MessageContentModel::initializeModel()
|
||||||
}
|
{
|
||||||
auto localPath = m_room->fileTransferInfo(m_event->id()).localPath.toLocalFile();
|
Q_ASSERT(m_room != nullptr);
|
||||||
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
|
// Allow making a model for an event that is being downloaded but will appear later
|
||||||
config.writePathEntry(mxcUrl.mid(6), localPath);
|
// e.g. a reply, but we need an ID to know when it has arrived.
|
||||||
}
|
Q_ASSERT(!m_eventId.isEmpty());
|
||||||
});
|
|
||||||
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
|
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
|
||||||
if (m_event != nullptr && eventId == m_event->id()) {
|
if (m_room != nullptr) {
|
||||||
|
if (eventId == m_eventId) {
|
||||||
|
m_event = m_room->getEvent(eventId);
|
||||||
|
Q_EMIT eventUpdated();
|
||||||
|
updateReplyModel();
|
||||||
updateComponents();
|
updateComponents();
|
||||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
return true;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
|
return false;
|
||||||
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();
|
if (m_event == nullptr) {
|
||||||
updateComponents(newEventId == m_event->id());
|
m_room->downloadEventFromServer(m_eventId);
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
|
|
||||||
updateComponents();
|
|
||||||
});
|
|
||||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, [this]() {
|
|
||||||
updateComponents();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
Q_EMIT eventUpdated();
|
||||||
|
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;
|
||||||
|
Q_EMIT eventUpdated();
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
|
||||||
|
if (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 && 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 && eventId == m_event->id()) {
|
||||||
|
updateComponents();
|
||||||
|
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||||
|
|
||||||
|
QString mxcUrl;
|
||||||
|
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
|
||||||
|
if (event->hasFileContent()) {
|
||||||
|
mxcUrl = event->content()->fileInfo()->url().toString();
|
||||||
|
}
|
||||||
|
} else if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
|
||||||
|
mxcUrl = event->image().fileInfo()->url().toString();
|
||||||
|
}
|
||||||
|
if (mxcUrl.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto localPath = m_room->fileTransferInfo(m_event->id()).localPath.toLocalFile();
|
||||||
|
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
|
||||||
|
config.writePathEntry(mxcUrl.mid(6), localPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
|
||||||
|
if (m_event != nullptr && eventId == m_event->id()) {
|
||||||
|
updateComponents();
|
||||||
|
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();
|
||||||
|
updateComponents(newEventId == m_event->id());
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
|
||||||
|
updateComponents();
|
||||||
|
});
|
||||||
|
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, [this]() {
|
||||||
|
updateComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (m_event != nullptr) {
|
||||||
|
updateReplyModel();
|
||||||
|
}
|
||||||
updateComponents();
|
updateComponents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +168,12 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
|||||||
const auto component = m_components[index.row()];
|
const auto component = m_components[index.row()];
|
||||||
|
|
||||||
if (role == DisplayRole) {
|
if (role == DisplayRole) {
|
||||||
|
if (component.type == MessageComponentType::Loading && m_isReply) {
|
||||||
|
return i18n("Loading reply");
|
||||||
|
}
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
if (m_event->isRedacted()) {
|
if (m_event->isRedacted()) {
|
||||||
auto reason = m_event->redactedBecause()->reason();
|
auto reason = m_event->redactedBecause()->reason();
|
||||||
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
|
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
|
||||||
@@ -184,20 +220,14 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
|||||||
if (role == IsReplyRole) {
|
if (role == IsReplyRole) {
|
||||||
return eventHandler.hasReply();
|
return eventHandler.hasReply();
|
||||||
}
|
}
|
||||||
if (role == ReplyComponentType) {
|
|
||||||
return eventHandler.replyMessageComponentType();
|
|
||||||
}
|
|
||||||
if (role == ReplyEventIdRole) {
|
if (role == ReplyEventIdRole) {
|
||||||
return eventHandler.getReplyId();
|
return eventHandler.getReplyId();
|
||||||
}
|
}
|
||||||
if (role == ReplyAuthorRole) {
|
if (role == ReplyAuthorRole) {
|
||||||
return eventHandler.getReplyAuthor();
|
return eventHandler.getReplyAuthor();
|
||||||
}
|
}
|
||||||
if (role == ReplyDisplayRole) {
|
if (role == ReplyContentModelRole) {
|
||||||
return eventHandler.getReplyRichBody();
|
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
|
||||||
}
|
|
||||||
if (role == ReplyMediaInfoRole) {
|
|
||||||
return eventHandler.getReplyMediaInfo();
|
|
||||||
}
|
}
|
||||||
if (role == LinkPreviewerRole) {
|
if (role == LinkPreviewerRole) {
|
||||||
if (component.type == MessageComponentType::LinkPreview) {
|
if (component.type == MessageComponentType::LinkPreview) {
|
||||||
@@ -233,11 +263,9 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
|
|||||||
roles[AssetRole] = "asset";
|
roles[AssetRole] = "asset";
|
||||||
roles[PollHandlerRole] = "pollHandler";
|
roles[PollHandlerRole] = "pollHandler";
|
||||||
roles[IsReplyRole] = "isReply";
|
roles[IsReplyRole] = "isReply";
|
||||||
roles[ReplyComponentType] = "replyComponentType";
|
|
||||||
roles[ReplyEventIdRole] = "replyEventId";
|
roles[ReplyEventIdRole] = "replyEventId";
|
||||||
roles[ReplyAuthorRole] = "replyAuthor";
|
roles[ReplyAuthorRole] = "replyAuthor";
|
||||||
roles[ReplyDisplayRole] = "replyDisplay";
|
roles[ReplyContentModelRole] = "replyContentModel";
|
||||||
roles[ReplyMediaInfoRole] = "replyMediaInfo";
|
|
||||||
roles[LinkPreviewerRole] = "linkPreviewer";
|
roles[LinkPreviewerRole] = "linkPreviewer";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
@@ -247,6 +275,12 @@ void MessageContentModel::updateComponents(bool isEditing)
|
|||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_components.clear();
|
m_components.clear();
|
||||||
|
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
|
||||||
|
endResetModel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (eventCast<const Quotient::RoomMessageEvent>(m_event)
|
if (eventCast<const Quotient::RoomMessageEvent>(m_event)
|
||||||
&& eventCast<const Quotient::RoomMessageEvent>(m_event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
|
&& eventCast<const Quotient::RoomMessageEvent>(m_event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
|
||||||
m_components += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
m_components += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
||||||
@@ -260,19 +294,14 @@ void MessageContentModel::updateComponents(bool isEditing)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
EventHandler eventHandler(m_room, m_event);
|
if (m_replyModel != nullptr) {
|
||||||
if (eventHandler.hasReply()) {
|
m_components += MessageComponent{MessageComponentType::Reply, QString(), {}};
|
||||||
if (m_room->findInTimeline(eventHandler.getReplyId()) == m_room->historyEdge()) {
|
|
||||||
m_components += MessageComponent{MessageComponentType::ReplyLoad, QString(), {}};
|
|
||||||
m_room->loadReply(m_event->id(), eventHandler.getReplyId());
|
|
||||||
} else {
|
|
||||||
m_components += MessageComponent{MessageComponentType::Reply, QString(), {}};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
m_components += MessageComponent{MessageComponentType::Edit, QString(), {}};
|
m_components += MessageComponent{MessageComponentType::Edit, QString(), {}};
|
||||||
} else {
|
} else {
|
||||||
|
EventHandler eventHandler(m_room, m_event);
|
||||||
m_components.append(componentsForType(eventHandler.messageComponentType()));
|
m_components.append(componentsForType(eventHandler.messageComponentType()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +312,29 @@ void MessageContentModel::updateComponents(bool isEditing)
|
|||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MessageContentModel::updateReplyModel()
|
||||||
|
{
|
||||||
|
if (m_event == nullptr || m_replyModel != nullptr || m_isReply) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventHandler eventHandler(m_room, m_event);
|
||||||
|
if (!eventHandler.hasReply()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto replyEvent = m_room->findInTimeline(eventHandler.getReplyId());
|
||||||
|
if (replyEvent == m_room->historyEdge()) {
|
||||||
|
m_replyModel = new MessageContentModel(m_room, eventHandler.getReplyId(), true);
|
||||||
|
} else {
|
||||||
|
m_replyModel = new MessageContentModel(m_room, replyEvent->get(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
|
||||||
|
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
|
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
|
||||||
{
|
{
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|||||||
@@ -58,17 +58,16 @@ public:
|
|||||||
PollHandlerRole, /**< The PollHandler for the event, if any. */
|
PollHandlerRole, /**< The PollHandler for the event, if any. */
|
||||||
|
|
||||||
IsReplyRole, /**< Is the message a reply to another event. */
|
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. */
|
ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */
|
||||||
ReplyAuthorRole, /**< The author of the event that was replied to. */
|
ReplyAuthorRole, /**< The author of the event that was replied to. */
|
||||||
ReplyDisplayRole, /**< The body of the message that was replied to. */
|
ReplyContentModelRole, /**< The MessageContentModel for the reply event. */
|
||||||
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
|
|
||||||
|
|
||||||
LinkPreviewerRole, /**< The link preview details. */
|
LinkPreviewerRole, /**< The link preview details. */
|
||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
explicit MessageContentModel(const Quotient::RoomEvent *event, NeoChatRoom *room);
|
explicit MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false);
|
||||||
|
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the given role value at the given index.
|
* @brief Get the given role value at the given index.
|
||||||
@@ -98,13 +97,24 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE void closeLinkPreview(int row);
|
Q_INVOKABLE void closeLinkPreview(int row);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void eventUpdated();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPointer<NeoChatRoom> m_room;
|
QPointer<NeoChatRoom> m_room;
|
||||||
|
QString m_eventId;
|
||||||
const Quotient::RoomEvent *m_event = nullptr;
|
const Quotient::RoomEvent *m_event = nullptr;
|
||||||
|
|
||||||
|
bool m_isReply;
|
||||||
|
|
||||||
|
void initializeModel();
|
||||||
|
|
||||||
QList<MessageComponent> m_components;
|
QList<MessageComponent> m_components;
|
||||||
void updateComponents(bool isEditing = false);
|
void updateComponents(bool isEditing = false);
|
||||||
|
|
||||||
|
QPointer<MessageContentModel> m_replyModel;
|
||||||
|
void updateReplyModel();
|
||||||
|
|
||||||
ItineraryModel *m_itineraryModel = nullptr;
|
ItineraryModel *m_itineraryModel = nullptr;
|
||||||
|
|
||||||
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
|
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
|
||||||
|
|||||||
@@ -441,11 +441,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
|
|
||||||
if (role == ContentModelRole) {
|
if (role == ContentModelRole) {
|
||||||
if (!evt.isStateEvent()) {
|
if (!evt.isStateEvent()) {
|
||||||
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&evt, m_currentRoom));
|
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_currentRoom, &evt));
|
||||||
}
|
}
|
||||||
if (evt.isStateEvent()) {
|
if (evt.isStateEvent()) {
|
||||||
if (evt.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
if (evt.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
||||||
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&evt, m_currentRoom));
|
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_currentRoom, &evt));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -113,11 +113,11 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
|||||||
return eventHandler.threadRoot();
|
return eventHandler.threadRoot();
|
||||||
case ContentModelRole: {
|
case ContentModelRole: {
|
||||||
if (!event.isStateEvent()) {
|
if (!event.isStateEvent()) {
|
||||||
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&event, m_room));
|
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_room, &event));
|
||||||
}
|
}
|
||||||
if (event.isStateEvent()) {
|
if (event.isStateEvent()) {
|
||||||
if (event.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
if (event.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
||||||
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&event, m_room));
|
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_room, &event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
|||||||
});
|
});
|
||||||
|
|
||||||
connect(this, &Room::addedMessages, this, &NeoChatRoom::readMarkerLoadedChanged);
|
connect(this, &Room::addedMessages, this, &NeoChatRoom::readMarkerLoadedChanged);
|
||||||
|
connect(this, &Room::aboutToAddHistoricalMessages, this, &NeoChatRoom::cleanupExtraEventRange);
|
||||||
|
connect(this, &Room::aboutToAddNewMessages, this, &NeoChatRoom::cleanupExtraEventRange);
|
||||||
|
|
||||||
const auto &roomLastMessageProvider = RoomLastMessageProvider::self();
|
const auto &roomLastMessageProvider = RoomLastMessageProvider::self();
|
||||||
|
|
||||||
@@ -1701,6 +1703,40 @@ QUrl NeoChatRoom::avatarForMember(Quotient::User *user) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::downloadEventFromServer(const QString &eventId)
|
||||||
|
{
|
||||||
|
if (findInTimeline(eventId) != historyEdge()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto job = connection()->callApi<GetOneRoomEventJob>(id(), eventId);
|
||||||
|
connect(job, &BaseJob::success, this, [this, job, eventId] {
|
||||||
|
// The event may have arrived in the meantime so check it's not in the timeline.
|
||||||
|
if (findInTimeline(eventId) != historyEdge()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event_ptr_tt<RoomEvent> event = fromJson<event_ptr_tt<RoomEvent>>(job->jsonData());
|
||||||
|
m_extraEvents.push_back(std::move(event));
|
||||||
|
Q_EMIT extraEventLoaded(eventId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const RoomEvent *NeoChatRoom::getEvent(const QString &eventId) const
|
||||||
|
{
|
||||||
|
if (eventId.isEmpty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const auto timelineIt = findInTimeline(eventId);
|
||||||
|
if (timelineIt != historyEdge()) {
|
||||||
|
return timelineIt->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto extraIt = std::find_if(m_extraEvents.begin(), m_extraEvents.end(), [eventId](const Quotient::event_ptr_tt<Quotient::RoomEvent> &event) {
|
||||||
|
return event->id() == eventId;
|
||||||
|
});
|
||||||
|
return extraIt != m_extraEvents.end() ? extraIt->get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
|
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
|
||||||
{
|
{
|
||||||
const QString &replyEventId = event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
const QString &replyEventId = event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
||||||
@@ -1721,13 +1757,22 @@ const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
|
|||||||
return replyPtr;
|
return replyPtr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NeoChatRoom::loadReply(const QString &eventId, const QString &replyId)
|
void NeoChatRoom::cleanupExtraEventRange(Quotient::RoomEventsRange events)
|
||||||
{
|
{
|
||||||
auto job = connection()->callApi<GetOneRoomEventJob>(id(), replyId);
|
for (auto &&event : events) {
|
||||||
connect(job, &BaseJob::success, this, [this, job, eventId, replyId] {
|
cleanupExtraEvent(event->id());
|
||||||
m_extraEvents.push_back(fromJson<event_ptr_tt<RoomEvent>>(job->jsonData()));
|
}
|
||||||
Q_EMIT replyLoaded(eventId, replyId);
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::cleanupExtraEvent(const QString &eventId)
|
||||||
|
{
|
||||||
|
auto it = std::find_if(m_extraEvents.begin(), m_extraEvents.end(), [eventId](Quotient::event_ptr_tt<Quotient::RoomEvent> &event) {
|
||||||
|
return event->id() == eventId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (it != m_extraEvents.end()) {
|
||||||
|
m_extraEvents.erase(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
User *NeoChatRoom::invitingUser() const
|
User *NeoChatRoom::invitingUser() const
|
||||||
|
|||||||
@@ -632,18 +632,31 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE [[nodiscard]] QUrl avatarForMember(Quotient::User *user) const;
|
Q_INVOKABLE [[nodiscard]] QUrl avatarForMember(Quotient::User *user) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Loads the event with the given id from the server and saves it locally.
|
||||||
|
*
|
||||||
|
* Intended to retrieve events that are needed, e.g. replied to events that are
|
||||||
|
* not currently in the timeline.
|
||||||
|
*
|
||||||
|
* If the event is already in the timeline nothing will happen.
|
||||||
|
*/
|
||||||
|
void downloadEventFromServer(const QString &eventId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the event with the given ID if available.
|
||||||
|
*
|
||||||
|
* This function will check both the timeline and extra events and return a
|
||||||
|
* non-nullptr value if it is found in either.
|
||||||
|
*
|
||||||
|
* The result will be nullptr if not found so needs to be managed.
|
||||||
|
*/
|
||||||
|
const Quotient::RoomEvent *getEvent(const QString &eventId) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply.
|
* @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply.
|
||||||
*/
|
*/
|
||||||
const Quotient::RoomEvent *getReplyForEvent(const Quotient::RoomEvent &event) const;
|
const Quotient::RoomEvent *getReplyForEvent(const Quotient::RoomEvent &event) const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the event replyId with the given id from the server and saves it locally.
|
|
||||||
* For models to update correctly, eventId must be the event that is replying to replyId.
|
|
||||||
* Intended to load the replied-to event when it isn't available locally.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void loadReply(const QString &eventId, const QString &replyId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If we're invited to this room, the user that invited us. Undefined in other cases.
|
* If we're invited to this room, the user that invited us. Undefined in other cases.
|
||||||
*/
|
*/
|
||||||
@@ -674,6 +687,8 @@ private:
|
|||||||
|
|
||||||
QCache<QString, PollHandler> m_polls;
|
QCache<QString, PollHandler> m_polls;
|
||||||
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_extraEvents;
|
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_extraEvents;
|
||||||
|
void cleanupExtraEventRange(Quotient::RoomEventsRange events);
|
||||||
|
void cleanupExtraEvent(const QString &eventId);
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void updatePushNotificationState(QString type);
|
void updatePushNotificationState(QString type);
|
||||||
@@ -703,7 +718,7 @@ Q_SIGNALS:
|
|||||||
void defaultUrlPreviewStateChanged();
|
void defaultUrlPreviewStateChanged();
|
||||||
void urlPreviewEnabledChanged();
|
void urlPreviewEnabledChanged();
|
||||||
void maxRoomVersionChanged();
|
void maxRoomVersionChanged();
|
||||||
void replyLoaded(const QString &eventId, const QString &replyId);
|
void extraEventLoaded(const QString &eventId);
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ qt_add_qml_module(timeline
|
|||||||
ReactionDelegate.qml
|
ReactionDelegate.qml
|
||||||
SectionDelegate.qml
|
SectionDelegate.qml
|
||||||
MessageComponentChooser.qml
|
MessageComponentChooser.qml
|
||||||
|
ReplyMessageComponentChooser.qml
|
||||||
AudioComponent.qml
|
AudioComponent.qml
|
||||||
CodeComponent.qml
|
CodeComponent.qml
|
||||||
EncryptedComponent.qml
|
EncryptedComponent.qml
|
||||||
@@ -32,6 +33,7 @@ qt_add_qml_module(timeline
|
|||||||
FlightReservationComponent.qml
|
FlightReservationComponent.qml
|
||||||
HotelReservationComponent.qml
|
HotelReservationComponent.qml
|
||||||
LinkPreviewComponent.qml
|
LinkPreviewComponent.qml
|
||||||
|
LinkPreviewLoadComponent.qml
|
||||||
LiveLocationComponent.qml
|
LiveLocationComponent.qml
|
||||||
LoadComponent.qml
|
LoadComponent.qml
|
||||||
LocationComponent.qml
|
LocationComponent.qml
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ QQC2.Control {
|
|||||||
/**
|
/**
|
||||||
* @brief The timestamp of the message.
|
* @brief The timestamp of the message.
|
||||||
*/
|
*/
|
||||||
required property var time
|
property date time
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The display text of the message.
|
* @brief The display text of the message.
|
||||||
@@ -135,6 +135,7 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
|
enabled: root.time.toString() !== "Invalid Date"
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onTapped: RoomManager.maximizeCode(root.author, root.time, root.display, root.componentAttributes.class)
|
onTapped: RoomManager.maximizeCode(root.author, root.time, root.display, root.componentAttributes.class)
|
||||||
onLongPressed: root.showMessageMenu()
|
onLongPressed: root.showMessageMenu()
|
||||||
@@ -174,6 +175,7 @@ QQC2.Control {
|
|||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
}
|
}
|
||||||
QQC2.Button {
|
QQC2.Button {
|
||||||
|
visible: root.time.toString() !== "Invalid Date"
|
||||||
icon.name: "view-fullscreen"
|
icon.name: "view-fullscreen"
|
||||||
text: i18nc("@action:button", "Maximize")
|
text: i18nc("@action:button", "Maximize")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|||||||
91
src/timeline/LinkPreviewLoadComponent.qml
Normal file
91
src/timeline/LinkPreviewLoadComponent.qml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// 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.
|
||||||
|
*/
|
||||||
|
QQC2.Control {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The index of the delegate in the model.
|
||||||
|
*/
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request for this delegate to be removed.
|
||||||
|
*/
|
||||||
|
signal remove(int index)
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
Reply,
|
||||||
|
LinkPreview
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
contentItem : RowLayout {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
width: Kirigami.Units.smallSpacing
|
||||||
|
color: Kirigami.Theme.highlightColor
|
||||||
|
}
|
||||||
|
QQC2.BusyIndicator {}
|
||||||
|
Kirigami.Heading {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumHeight: root.defaultHeight
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
level: 2
|
||||||
|
text: {
|
||||||
|
switch (root.type) {
|
||||||
|
case LinkPreviewLoadComponent.Reply:
|
||||||
|
return i18n("Loading reply");
|
||||||
|
case LinkPreviewLoadComponent.LinkPreview:
|
||||||
|
return i18n("Loading URL preview");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
id: closeButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
visible: root.hovered && root.type === LinkPreviewLoadComponent.LinkPreview
|
||||||
|
text: i18nc("As in remove the link preview so it's no longer shown", "Remove preview")
|
||||||
|
icon.name: "dialog-close"
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: root.remove(root.index)
|
||||||
|
|
||||||
|
QQC2.ToolTip {
|
||||||
|
text: closeButton.text
|
||||||
|
visible: closeButton.hovered
|
||||||
|
delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,84 +8,29 @@ import QtQuick.Layouts
|
|||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief A component to show a link preview loading from a message.
|
* @brief A component to show that part of a message is loading.
|
||||||
*/
|
*/
|
||||||
QQC2.Control {
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
/**
|
required property string display
|
||||||
* @brief The index of the delegate in the model.
|
|
||||||
*/
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
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.
|
* @brief The maximum width that the bubble's content can be.
|
||||||
*/
|
*/
|
||||||
property real maxContentWidth: -1
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Request for this delegate to be removed.
|
|
||||||
*/
|
|
||||||
signal remove(int index)
|
|
||||||
|
|
||||||
enum Type {
|
|
||||||
Reply,
|
|
||||||
LinkPreview
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumWidth: root.maxContentWidth
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
contentItem : RowLayout {
|
QQC2.BusyIndicator {}
|
||||||
spacing: Kirigami.Units.smallSpacing
|
Kirigami.Heading {
|
||||||
|
id: loadingText
|
||||||
Rectangle {
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
verticalAlignment: Text.AlignVCenter
|
||||||
width: Kirigami.Units.smallSpacing
|
level: 2
|
||||||
color: Kirigami.Theme.highlightColor
|
text: root.display.length > 0 ? root.display : i18n("Loading")
|
||||||
}
|
|
||||||
QQC2.BusyIndicator {}
|
|
||||||
Kirigami.Heading {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.minimumHeight: root.defaultHeight
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
level: 2
|
|
||||||
text: {
|
|
||||||
switch (root.type) {
|
|
||||||
case LoadComponent.Reply:
|
|
||||||
return i18n("Loading reply");
|
|
||||||
case LoadComponent.LinkPreview:
|
|
||||||
return i18n("Loading URL preview");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Button {
|
|
||||||
id: closeButton
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
visible: root.hovered && root.type === LoadComponent.LinkPreview
|
|
||||||
text: i18nc("As in remove the link preview so it's no longer shown", "Remove preview")
|
|
||||||
icon.name: "dialog-close"
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
|
|
||||||
onClicked: root.remove(root.index)
|
|
||||||
|
|
||||||
QQC2.ToolTip {
|
|
||||||
text: closeButton.text
|
|
||||||
visible: closeButton.hovered
|
|
||||||
delay: Kirigami.Units.toolTipDelay
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,17 +183,6 @@ DelegateChooser {
|
|||||||
onReplyClicked: eventId => {
|
onReplyClicked: eventId => {
|
||||||
root.replyClicked(eventId);
|
root.replyClicked(eventId);
|
||||||
}
|
}
|
||||||
onSelectedTextChanged: selectedText => {
|
|
||||||
root.selectedTextChanged(selectedText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: MessageComponentType.ReplyLoad
|
|
||||||
delegate: LoadComponent {
|
|
||||||
type: LoadComponent.Reply
|
|
||||||
maxContentWidth: root.maxContentWidth
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,8 +196,8 @@ DelegateChooser {
|
|||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageComponentType.LinkPreviewLoad
|
roleValue: MessageComponentType.LinkPreviewLoad
|
||||||
delegate: LoadComponent {
|
delegate: LinkPreviewLoadComponent {
|
||||||
type: LoadComponent.LinkPreview
|
type: LinkPreviewLoadComponent.LinkPreview
|
||||||
maxContentWidth: root.maxContentWidth
|
maxContentWidth: root.maxContentWidth
|
||||||
onRemove: index => root.removeLinkPreview(index)
|
onRemove: index => root.removeLinkPreview(index)
|
||||||
}
|
}
|
||||||
@@ -231,6 +220,13 @@ DelegateChooser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Loading
|
||||||
|
delegate: LoadComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageComponentType.Other
|
roleValue: MessageComponentType.Other
|
||||||
delegate: Item {}
|
delegate: Item {}
|
||||||
|
|||||||
@@ -24,11 +24,6 @@ import org.kde.neochat
|
|||||||
RowLayout {
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The matrix ID of the reply event.
|
|
||||||
*/
|
|
||||||
required property var replyComponentType
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The matrix ID of the reply event.
|
* @brief The matrix ID of the reply event.
|
||||||
*/
|
*/
|
||||||
@@ -53,26 +48,9 @@ RowLayout {
|
|||||||
required property var replyAuthor
|
required property var replyAuthor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The display text of the message replied to.
|
* @brief The model to visualise the content of the message replied to.
|
||||||
*/
|
*/
|
||||||
required property string replyDisplay
|
required property var replyContentModel
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The media info for the reply event.
|
|
||||||
*
|
|
||||||
* This could be an image, audio, video or file.
|
|
||||||
*
|
|
||||||
* This should consist of the following:
|
|
||||||
* - source - The mxc URL for the media.
|
|
||||||
* - mimeType - The MIME type of the media.
|
|
||||||
* - mimeIcon - The MIME icon name.
|
|
||||||
* - size - The file size in bytes.
|
|
||||||
* - duration - The length in seconds of the audio media (audio/video only).
|
|
||||||
* - width - The width in pixels of the audio media (image/video only).
|
|
||||||
* - height - The height in pixels of the audio media (image/video only).
|
|
||||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
|
|
||||||
*/
|
|
||||||
required property var replyMediaInfo
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The maximum width that the bubble's content can be.
|
* @brief The maximum width that the bubble's content can be.
|
||||||
@@ -84,12 +62,6 @@ RowLayout {
|
|||||||
*/
|
*/
|
||||||
signal replyClicked(string eventID)
|
signal replyClicked(string eventID)
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The user selected text has changed.
|
|
||||||
*/
|
|
||||||
signal selectedTextChanged(string selectedText)
|
|
||||||
|
|
||||||
implicitHeight: contentColumn.implicitHeight
|
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -101,7 +73,6 @@ RowLayout {
|
|||||||
}
|
}
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: contentColumn
|
id: contentColumn
|
||||||
implicitHeight: headerRow.implicitHeight + (root.replyComponentType != MessageComponentType.Other ? contentRepeater.itemAt(0).implicitHeight + spacing : 0)
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -131,75 +102,11 @@ RowLayout {
|
|||||||
}
|
}
|
||||||
Repeater {
|
Repeater {
|
||||||
id: contentRepeater
|
id: contentRepeater
|
||||||
model: [root.replyComponentType]
|
model: root.replyContentModel
|
||||||
delegate: DelegateChooser {
|
delegate: ReplyMessageComponentChooser {
|
||||||
role: "modelData"
|
maxContentWidth: _private.availableContentWidth
|
||||||
|
|
||||||
DelegateChoice {
|
onReplyClicked: root.replyClicked(root.replyEventId)
|
||||||
roleValue: MessageComponentType.Text
|
|
||||||
delegate: TextComponent {
|
|
||||||
display: root.replyDisplay
|
|
||||||
maxContentWidth: _private.availableContentWidth
|
|
||||||
|
|
||||||
onSelectedTextChanged: root.selectedTextChanged(selectedText)
|
|
||||||
|
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
170
src/timeline/ReplyMessageComponentChooser.qml
Normal file
170
src/timeline/ReplyMessageComponentChooser.qml
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
// 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 QtQuick.Layouts
|
||||||
|
import Qt.labs.qmlmodels
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select a message component based on a MessageComponentType.
|
||||||
|
*/
|
||||||
|
DelegateChooser {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The reply has been clicked.
|
||||||
|
*/
|
||||||
|
signal replyClicked()
|
||||||
|
|
||||||
|
role: "componentType"
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Text
|
||||||
|
delegate: TextComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.replyClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Image
|
||||||
|
delegate: Image {
|
||||||
|
id: image
|
||||||
|
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
|
Layout.maximumWidth: mediaSizeHelper.currentSize.width
|
||||||
|
Layout.maximumHeight: mediaSizeHelper.currentSize.height
|
||||||
|
source: image.mediaInfo.source
|
||||||
|
|
||||||
|
MediaSizeHelper {
|
||||||
|
id: mediaSizeHelper
|
||||||
|
contentMaxWidth: root.maxContentWidth
|
||||||
|
mediaWidth: image.mediaInfo.width ?? 0
|
||||||
|
mediaHeight: image.mediaInfo.height ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Video
|
||||||
|
delegate: MimeComponent {
|
||||||
|
required property string display
|
||||||
|
required property var mediaInfo
|
||||||
|
required property int componentType
|
||||||
|
|
||||||
|
mimeIconSource: mediaInfo.mimeIcon
|
||||||
|
label: display
|
||||||
|
subLabel: componentType === MessageComponentType.File ? Format.formatByteSize(mediaInfo.size) : Format.formatDuration(mediaInfo.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Code
|
||||||
|
delegate: CodeComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.replyClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Quote
|
||||||
|
delegate: QuoteComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.replyClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Audio
|
||||||
|
delegate: MimeComponent {
|
||||||
|
required property string display
|
||||||
|
required property var mediaInfo
|
||||||
|
required property int componentType
|
||||||
|
|
||||||
|
mimeIconSource: mediaInfo.mimeIcon
|
||||||
|
label: display
|
||||||
|
subLabel: componentType === MessageComponentType.File ? Format.formatByteSize(mediaInfo.size) : Format.formatDuration(mediaInfo.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.File
|
||||||
|
delegate: MimeComponent {
|
||||||
|
required property string display
|
||||||
|
required property var mediaInfo
|
||||||
|
required property int componentType
|
||||||
|
|
||||||
|
mimeIconSource: mediaInfo.mimeIcon
|
||||||
|
label: display
|
||||||
|
subLabel: componentType === MessageComponentType.File ? Format.formatByteSize(mediaInfo.size) : Format.formatDuration(mediaInfo.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Loading
|
||||||
|
delegate: LoadComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Other
|
||||||
|
delegate: Item {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user