// SPDX-FileCopyrightText: 2024 James Graham // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "messagecontentmodel.h" #include #include #include "chatbarcache.h" #include "contentprovider.h" #include "neochatconnection.h" #include "texthandler.h" using namespace Quotient; MessageContentModel::MessageContentModel(NeoChatRoom *room, MessageContentModel *parent, const QString &eventId) : QAbstractListModel(parent) , m_room(room) , m_eventId(eventId) { connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &MessageContentModel::updateSpoilers); initializeModel(); } void MessageContentModel::initializeModel() { Q_ASSERT(m_room != nullptr); connect(this, &MessageContentModel::componentsUpdated, this, [this]() { if (m_room->urlPreviewEnabled()) { forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewAddFunction); } else { forEachComponentOfType({MessageComponentType::LinkPreview, MessageComponentType::LinkPreviewLoad}, m_linkPreviewRemoveFunction); } m_components.squeeze(); }); connect(this, &MessageContentModel::itineraryUpdated, this, [this]() { if (hasComponentType(MessageComponentType::File)) { forEachComponentOfType(MessageComponentType::File, m_fileFunction); } }); connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) { if (eventId == m_eventId) { forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video}, m_fileInfoFunction); } }); connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) { if (eventId == m_eventId) { forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video}, m_fileInfoFunction); } }); connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) { if (m_room != nullptr && eventId == m_eventId) { forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video}, m_fileInfoFunction); } }); connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId, const QString &errorMessage) { if (eventId == m_eventId) { forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video}, m_fileInfoFunction); if (errorMessage.isEmpty()) { Q_EMIT m_room->showMessage(MessageType::Error, i18nc("@info", "Failed to download file.")); } else { Q_EMIT m_room->showMessage(MessageType::Error, i18nc("@info Failed to download file: [error message]", "Failed to download file:
%1", errorMessage)); } } }); connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, &MessageContentModel::componentsUpdated); } QString MessageContentModel::eventId() const { return m_eventId; } QDateTime MessageContentModel::time() const { return QDateTime::currentDateTime(); } QString MessageContentModel::timeString() const { return time().toLocalTime().toString(u"hh:mm"_s); ; } QString MessageContentModel::authorId() const { return m_room->localMember().id(); } NeochatRoomMember *MessageContentModel::author() const { return m_room->qmlSafeMember(authorId()); } QString MessageContentModel::threadRootId() const { return {}; } 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 {}; } if (!m_room) { qWarning() << "MessageContentModel::data called without room"; return {}; } const auto component = m_components[index.row()]; if (role == DisplayRole) { return component.display; } if (role == ComponentTypeRole) { return component.type; } if (role == ComponentAttributesRole) { return component.attributes; } if (role == EventIdRole) { return eventId(); } if (role == TimeRole) { return time(); } if (role == TimeStringRole) { return timeString(); } if (role == AuthorRole) { return QVariant::fromValue(author()); } if (role == FileTransferInfoRole) { return QVariant::fromValue(m_room->cachedFileTransferInfo(m_eventId)); } if (role == ItineraryModelRole) { return QVariant::fromValue(m_itineraryModel); } if (role == PollHandlerRole) { return QVariant::fromValue(ContentProvider::self().handlerForPoll(m_room, m_eventId)); } if (role == ReplyContentModelRole) { return QVariant::fromValue(m_replyModel); } if (role == ReactionModelRole) { return QVariant::fromValue(m_reactionModel); } if (role == ThreadRootRole) { return threadRootId(); } if (role == LinkPreviewerRole) { if (component.type == MessageComponentType::LinkPreview) { return QVariant::fromValue( dynamic_cast(m_room->connection())->previewerForLink(component.attributes["link"_L1].toUrl())); } else { return QVariant::fromValue(emptyLinkPreview); } } if (role == ChatBarCacheRole) { if (m_room->threadCache()->threadId() == m_eventId) { return QVariant::fromValue(m_room->threadCache()); } return QVariant::fromValue(m_room->editCache()); } return {}; } int MessageContentModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_components.size(); } QHash MessageContentModel::roleNames() const { return roleNamesStatic(); } QHash MessageContentModel::roleNamesStatic() { QHash roles; roles[MessageContentModel::DisplayRole] = "display"; roles[MessageContentModel::ComponentTypeRole] = "componentType"; roles[MessageContentModel::ComponentAttributesRole] = "componentAttributes"; roles[MessageContentModel::EventIdRole] = "eventId"; roles[MessageContentModel::TimeRole] = "time"; roles[MessageContentModel::TimeStringRole] = "timeString"; roles[MessageContentModel::AuthorRole] = "author"; roles[MessageContentModel::FileTransferInfoRole] = "fileTransferInfo"; roles[MessageContentModel::ItineraryModelRole] = "itineraryModel"; roles[MessageContentModel::PollHandlerRole] = "pollHandler"; roles[MessageContentModel::ReplyContentModelRole] = "replyContentModel"; roles[MessageContentModel::ReactionModelRole] = "reactionModel"; roles[MessageContentModel::ThreadRootRole] = "threadRoot"; roles[MessageContentModel::LinkPreviewerRole] = "linkPreviewer"; roles[MessageContentModel::ChatBarCacheRole] = "chatBarCache"; return roles; } bool MessageContentModel::hasComponentType(MessageComponentType::Type type) { return std::find_if(m_components.cbegin(), m_components.cend(), [type](const MessageComponent &component) { return component.type == type; }) != m_components.cend(); } void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type, std::function function) { auto it = m_components.begin(); while ((it = std::find_if(it, m_components.end(), [type](const MessageComponent &component) { return component.type == type; })) != m_components.end()) { it = function(it); } } void MessageContentModel::forEachComponentOfType(QList types, std::function function) { for (const auto &type : types) { forEachComponentOfType(type, function); } } MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link) { const auto linkPreviewer = dynamic_cast(m_room->connection())->previewerForLink(link); if (linkPreviewer == nullptr) { return {}; } if (linkPreviewer->loaded()) { return MessageComponent{MessageComponentType::LinkPreview, QString(), {{"link"_L1, link}}}; } connect(linkPreviewer, &LinkPreviewer::loadedChanged, this, [this, link]() { const auto linkPreviewer = dynamic_cast(m_room->connection())->previewerForLink(link); if (linkPreviewer != nullptr && linkPreviewer->loaded()) { forEachComponentOfType(MessageComponentType::LinkPreviewLoad, [this, link](ComponentIt it) { if (it->attributes["link"_L1].toUrl() == link) { it->type = MessageComponentType::LinkPreview; Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {ComponentTypeRole}); } return ++it; }); } }); return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_L1, link}}}; } void MessageContentModel::closeLinkPreview(int row) { if (row < 0 || row >= m_components.size()) { qWarning() << "closeLinkPreview() called with row" << row << "which does not exist. m_components.size() =" << m_components.size(); return; } if (m_components[row].type == MessageComponentType::LinkPreview || m_components[row].type == MessageComponentType::LinkPreviewLoad) { beginRemoveRows({}, row, row); m_removedLinkPreviews += m_components[row].attributes["link"_L1].toUrl(); m_components.remove(row); m_components.squeeze(); endRemoveRows(); } } void MessageContentModel::updateSpoilers() { for (auto it = m_components.begin(); it != m_components.end(); ++it) { updateSpoiler(index(it - m_components.begin())); } } void MessageContentModel::updateSpoiler(const QModelIndex &index) { const auto row = index.row(); if (row < 0 || row >= rowCount()) { qWarning() << __FUNCTION__ << "called with row" << row << "which does not exist. m_components.size() =" << m_components.size(); return; } const auto spoilerRevealed = m_components[row].attributes.value("spoilerRevealed"_L1, false).toBool(); m_components[row].display = TextHandler::updateSpoilerText(this, m_components[row].display, spoilerRevealed); Q_EMIT dataChanged(index, index, {DisplayRole}); } void MessageContentModel::toggleSpoiler(QModelIndex index) { const auto row = index.row(); if (row < 0 || row >= rowCount()) { qWarning() << __FUNCTION__ << "called with row" << row << "which does not exist. m_components.size() =" << m_components.size(); return; } if (m_components[row].type != MessageComponentType::Text) { return; } const auto spoilerRevealed = !m_components[row].attributes.value("spoilerRevealed"_L1, false).toBool(); m_components[row].attributes["spoilerRevealed"_L1] = spoilerRevealed; Q_EMIT dataChanged(index, index, {ComponentAttributesRole}); updateSpoiler(index); } #include "moc_messagecontentmodel.cpp"