diff --git a/src/models/messagecontentmodel.cpp b/src/models/messagecontentmodel.cpp index be9e45fb5..8b605cda1 100644 --- a/src/models/messagecontentmodel.cpp +++ b/src/models/messagecontentmodel.cpp @@ -34,7 +34,7 @@ MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &event : QAbstractListModel(parent) , m_room(room) , m_eventId(eventId) - , m_isPending(isPending) + , m_currentState(isPending ? Pending : Unknown) , m_isReply(isReply) { initializeModel(); @@ -45,19 +45,27 @@ void MessageContentModel::initializeModel() Q_ASSERT(m_room != nullptr); Q_ASSERT(!m_eventId.isEmpty()); - connect(this, &MessageContentModel::eventUnavailable, this, &MessageContentModel::getEvent); - + connect(m_room, &NeoChatRoom::pendingEventAdded, this, [this]() { + if (m_room != nullptr && m_currentState == Unknown) { + initializeEvent(); + updateReplyModel(); + resetModel(); + } + }); connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) { if (m_room != nullptr) { if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) { - beginResetModel(); - m_isPending = false; m_eventId = serverEvent->id(); - initializeEvent(); - endResetModel(); } } }); + connect(m_room, &NeoChatRoom::pendingEventMerged, this, [this]() { + if (m_room != nullptr && m_currentState == Pending) { + initializeEvent(); + updateReplyModel(); + resetModel(); + } + }); connect(m_room, &NeoChatRoom::addedMessages, this, [this](int fromIndex, int toIndex) { if (m_room != nullptr) { for (int i = fromIndex; i <= toIndex; i++) { @@ -143,20 +151,33 @@ void MessageContentModel::initializeModel() }); initializeEvent(); - updateReplyModel(); - resetModel(); + if (m_currentState == Available || m_currentState == Pending) { + updateReplyModel(); + resetModel(); + } } void MessageContentModel::initializeEvent() { - const auto event = m_room->getEvent(m_eventId); - if (event == nullptr) { - Q_EMIT eventUnavailable(); + if (m_currentState == UnAvailable) { return; } + const auto eventResult = m_room->getEvent(m_eventId); + if (eventResult.first == nullptr) { + if (m_currentState != Pending) { + getEvent(); + } + return; + } + if (eventResult.second) { + m_currentState = Pending; + } else { + m_currentState = Available; + } + if (m_eventSenderObject == nullptr) { - auto senderId = event->senderId(); + auto senderId = eventResult.first->senderId(); // A pending event might not have a sender ID set yet but in that case it must // be the local member. if (senderId.isEmpty()) { @@ -172,7 +193,6 @@ void MessageContentModel::getEvent() Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) { if (m_room != nullptr) { if (eventId == m_eventId) { - m_notFound = false; initializeEvent(); updateReplyModel(); resetModel(); @@ -184,7 +204,7 @@ void MessageContentModel::getEvent() Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) { if (m_room != nullptr) { if (eventId == m_eventId) { - m_notFound = true; + m_currentState = UnAvailable; resetModel(); return true; } @@ -237,7 +257,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const const auto component = m_components[index.row()]; const auto event = m_room->getEvent(m_eventId); - if (event == nullptr) { + if (event.first == nullptr) { if (role == DisplayRole) { if (m_isReply) { return i18n("Loading reply"); @@ -252,7 +272,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const } if (role == DisplayRole) { - if (m_notFound || m_room->connection()->isIgnored(m_eventSenderId)) { + if (m_currentState == UnAvailable || m_room->connection()->isIgnored(m_eventSenderId)) { Kirigami::Platform::PlatformTheme *theme = static_cast(qmlAttachedPropertiesObject(this, true)); @@ -276,7 +296,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const if (!component.content.isEmpty()) { return component.content; } - return EventHandler::richBody(m_room, event); + return EventHandler::richBody(m_room, event.first); } if (role == ComponentTypeRole) { return component.type; @@ -285,53 +305,53 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const return component.attributes; } if (role == EventIdRole) { - return EventHandler::id(event); + return EventHandler::id(event.first); } if (role == TimeRole) { const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) { - return event->transactionId() == pendingEvent->transactionId(); + return event.first->transactionId() == pendingEvent->transactionId(); }); auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated(); - return EventHandler::time(event, m_isPending, lastUpdated); + return EventHandler::time(event.first, m_currentState == Pending, lastUpdated); } if (role == TimeStringRole) { const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) { - return event->transactionId() == pendingEvent->transactionId(); + return event.first->transactionId() == pendingEvent->transactionId(); }); auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated(); - return EventHandler::timeString(event, QStringLiteral("hh:mm"), m_isPending, lastUpdated); + return EventHandler::timeString(event.first, QStringLiteral("hh:mm"), m_currentState == Pending, lastUpdated); } if (role == AuthorRole) { return QVariant::fromValue(m_eventSenderObject.get()); } if (role == MediaInfoRole) { - return EventHandler::mediaInfo(m_room, event); + return EventHandler::mediaInfo(m_room, event.first); } if (role == FileTransferInfoRole) { - return QVariant::fromValue(m_room->cachedFileTransferInfo(event)); + return QVariant::fromValue(m_room->cachedFileTransferInfo(event.first)); } if (role == ItineraryModelRole) { return QVariant::fromValue(m_itineraryModel); } if (role == LatitudeRole) { - return EventHandler::latitude(event); + return EventHandler::latitude(event.first); } if (role == LongitudeRole) { - return EventHandler::longitude(event); + return EventHandler::longitude(event.first); } if (role == AssetRole) { - return EventHandler::locationAssetType(event); + return EventHandler::locationAssetType(event.first); } if (role == PollHandlerRole) { return QVariant::fromValue(m_room->poll(m_eventId)); } if (role == ReplyEventIdRole) { - return EventHandler::replyId(event); + return EventHandler::replyId(event.first); } if (role == ReplyAuthorRole) { - return QVariant::fromValue(EventHandler::replyAuthor(m_room, event)); + return QVariant::fromValue(EventHandler::replyAuthor(m_room, event.first)); } if (role == ReplyContentModelRole) { return QVariant::fromValue(m_replyModel); @@ -387,18 +407,17 @@ QHash MessageContentModel::roleNames() const void MessageContentModel::resetModel() { - const auto event = m_room->getEvent(m_eventId); - beginResetModel(); m_components.clear(); - if (m_room->connection()->isIgnored(m_eventSenderId) || m_notFound) { + if (m_room->connection()->isIgnored(m_eventSenderId) || m_currentState == UnAvailable) { m_components += MessageComponent{MessageComponentType::Text, QString(), {}}; endResetModel(); return; } - if (event == nullptr) { + const auto event = m_room->getEvent(m_eventId); + if (event.first == nullptr) { m_components += MessageComponent{MessageComponentType::Loading, QString(), {}}; endResetModel(); return; @@ -431,19 +450,19 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading) QList MessageContentModel::messageContentComponents(bool isEditing, bool isThreading) { const auto event = m_room->getEvent(m_eventId); - if (event == nullptr) { + if (event.first == nullptr) { return {}; } QList newComponents; - if (eventCast(event) - && eventCast(event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) { + if (eventCast(event.first) + && eventCast(event.first)->rawMsgtype() == QStringLiteral("m.key.verification.request")) { newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}}; return newComponents; } - if (event->isRedacted()) { + if (event.first->isRedacted()) { newComponents += MessageComponent{MessageComponentType::Text, QString(), {}}; return newComponents; } @@ -455,7 +474,7 @@ QList MessageContentModel::messageContentComponents(bool isEdi if (isEditing) { newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}}; } else { - newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event))); + newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first))); } if (m_room->urlPreviewEnabled()) { @@ -463,7 +482,7 @@ QList MessageContentModel::messageContentComponents(bool isEdi } // If the event is already threaded the ThreadModel will handle displaying a chat bar. - if (isThreading && !EventHandler::isThreaded(event)) { + if (isThreading && !EventHandler::isThreaded(event.first)) { newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}}; } @@ -473,11 +492,11 @@ QList MessageContentModel::messageContentComponents(bool isEdi void MessageContentModel::updateReplyModel() { const auto event = m_room->getEvent(m_eventId); - if (event == nullptr || m_isReply) { + if (event.first == nullptr || m_isReply) { return; } - if (!EventHandler::hasReply(event) || (EventHandler::isThreaded(event) && NeoChatConfig::self()->threads())) { + if (!EventHandler::hasReply(event.first) || (EventHandler::isThreaded(event.first) && NeoChatConfig::self()->threads())) { if (m_replyModel) { delete m_replyModel; } @@ -488,7 +507,7 @@ void MessageContentModel::updateReplyModel() return; } - m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(event), true, false, this); + m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(event.first), true, false, this); connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() { Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole}); @@ -498,13 +517,13 @@ void MessageContentModel::updateReplyModel() QList MessageContentModel::componentsForType(MessageComponentType::Type type) { const auto event = m_room->getEvent(m_eventId); - if (event == nullptr) { + if (event.first == nullptr) { return {}; } switch (type) { case MessageComponentType::Text: { - const auto roomMessageEvent = eventCast(event); + const auto roomMessageEvent = eventCast(event.first); auto body = EventHandler::rawMessageBody(*roomMessageEvent); return TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*roomMessageEvent), @@ -515,11 +534,11 @@ QList MessageContentModel::componentsForType(MessageComponentT case MessageComponentType::File: { QList components; components += MessageComponent{MessageComponentType::File, QString(), {}}; - const auto roomMessageEvent = eventCast(event); + const auto roomMessageEvent = eventCast(event.first); if (m_emptyItinerary) { if (!m_isReply) { - auto fileTransferInfo = m_room->cachedFileTransferInfo(event); + auto fileTransferInfo = m_room->cachedFileTransferInfo(event.first); #ifndef Q_OS_ANDROID Q_ASSERT(roomMessageEvent->content() != nullptr && roomMessageEvent->has()); @@ -567,8 +586,8 @@ QList MessageContentModel::componentsForType(MessageComponentT case MessageComponentType::Image: case MessageComponentType::Audio: case MessageComponentType::Video: { - if (!event->is()) { - const auto roomMessageEvent = eventCast(event); + if (!event.first->is()) { + const auto roomMessageEvent = eventCast(event.first); QList components; components += MessageComponent{type, QString(), {}}; auto body = EventHandler::rawMessageBody(*roomMessageEvent); @@ -653,13 +672,13 @@ void MessageContentModel::closeLinkPreview(int row) void MessageContentModel::updateItineraryModel() { const auto event = m_room->getEvent(m_eventId); - if (m_room == nullptr || event == nullptr) { + if (m_room == nullptr || event.first == nullptr) { return; } - if (auto roomMessageEvent = eventCast(event)) { + if (auto roomMessageEvent = eventCast(event.first)) { if (roomMessageEvent->has()) { - auto filePath = m_room->cachedFileTransferInfo(event).localPath; + auto filePath = m_room->cachedFileTransferInfo(event.first).localPath; if (filePath.isEmpty() && m_itineraryModel != nullptr) { delete m_itineraryModel; m_itineraryModel = nullptr; diff --git a/src/models/messagecontentmodel.h b/src/models/messagecontentmodel.h index d07e7e79e..877702445 100644 --- a/src/models/messagecontentmodel.h +++ b/src/models/messagecontentmodel.h @@ -31,6 +31,14 @@ class MessageContentModel : public QAbstractListModel Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged) public: + enum MessageState { + Unknown, /**< The message state is unknown. */ + Pending, /**< The message is a new pending message which the server has not yet acknowledged. */ + Available, /**< The message is available and acknowledged by the server. */ + UnAvailable, /**< The message can't be retrieved either because it doesn't exist or is blocked. */ + }; + Q_ENUM(MessageState) + /** * @brief Defines the model roles. */ @@ -98,7 +106,6 @@ public: Q_SIGNALS: void showAuthorChanged(); - void eventUnavailable(); void eventUpdated(); private: @@ -107,10 +114,9 @@ private: QString m_eventSenderId; std::unique_ptr m_eventSenderObject = nullptr; - bool m_isPending; + MessageState m_currentState = Unknown; bool m_showAuthor = true; bool m_isReply; - bool m_notFound = false; void initializeModel(); void initializeEvent(); diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp index d06869f27..68cc812f5 100644 --- a/src/models/messageeventmodel.cpp +++ b/src/models/messageeventmodel.cpp @@ -162,7 +162,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room) }); connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) { m_initialized = true; - createEventObjects(event); + createEventObjects(event, true); beginInsertRows({}, 0, 0); }); connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows); @@ -618,7 +618,7 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex(); } -void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event) +void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event, bool isPending) { if (event == nullptr) { return; @@ -641,7 +641,7 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event) if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) { if (!event->isStateEvent() || event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) { - m_contentModels[eventId] = std::unique_ptr(new MessageContentModel(m_currentRoom, eventId)); + m_contentModels[eventId] = std::unique_ptr(new MessageContentModel(m_currentRoom, eventId, false, isPending)); } } diff --git a/src/models/messageeventmodel.h b/src/models/messageeventmodel.h index dc90923c2..43e0f3afc 100644 --- a/src/models/messageeventmodel.h +++ b/src/models/messageeventmodel.h @@ -136,7 +136,7 @@ private: int refreshEventRoles(const QString &eventId, const QList &roles = {}); void moveReadMarker(const QString &toEventId); - void createEventObjects(const Quotient::RoomEvent *event); + void createEventObjects(const Quotient::RoomEvent *event, bool isPending = false); // Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows bool m_initialized = false; diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index 3d4343069..0dcd1fcd2 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -1749,25 +1749,31 @@ void NeoChatRoom::downloadEventFromServer(const QString &eventId) }); } -const RoomEvent *NeoChatRoom::getEvent(const QString &eventId) const +std::pair NeoChatRoom::getEvent(const QString &eventId) const { if (eventId.isEmpty()) { - return nullptr; + return {}; } const auto timelineIt = findInTimeline(eventId); if (timelineIt != historyEdge()) { - return timelineIt->get(); + return std::make_pair(timelineIt->get(), false); } - const auto pendingIt = findPendingEvent(eventId); + auto pendingIt = findPendingEvent(eventId); if (pendingIt != pendingEvents().end()) { - return pendingIt->event(); + return std::make_pair(pendingIt->event(), true); + } + // findPendingEvent() searches by transaction ID, we also need to check event ID. + for (const auto &event : pendingEvents()) { + if (event->id() == eventId || event->transactionId() == eventId) { + return std::make_pair(event.event(), true); + } } auto extraIt = std::find_if(m_extraEvents.begin(), m_extraEvents.end(), [eventId](const Quotient::event_ptr_tt &event) { return event->id() == eventId; }); - return extraIt != m_extraEvents.end() ? extraIt->get() : nullptr; + return std::make_pair(extraIt != m_extraEvents.end() ? extraIt->get() : nullptr, false); } const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const diff --git a/src/neochatroom.h b/src/neochatroom.h index cfbacecd7..80b6ea582 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -570,7 +570,7 @@ public: * * The result will be nullptr if not found so needs to be managed. */ - const Quotient::RoomEvent *getEvent(const QString &eventId) const; + std::pair getEvent(const QString &eventId) const; /** * @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply.