diff --git a/autotests/chatbarcachetest.cpp b/autotests/chatbarcachetest.cpp index 3dfc50233..fbfa00838 100644 --- a/autotests/chatbarcachetest.cpp +++ b/autotests/chatbarcachetest.cpp @@ -107,8 +107,13 @@ void ChatBarCacheTest::reply() void ChatBarCacheTest::edit() { QScopedPointer chatBarCache(new ChatBarCache(room)); + chatBarCache->setText(QLatin1String("some text")); chatBarCache->setAttachmentPath(QLatin1String("some/path")); + connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [](const QString &oldEventId, const QString &newEventId) { + QCOMPARE(oldEventId, QString()); + QCOMPARE(newEventId, QString(QLatin1String("$153456789:example.org"))); + }); chatBarCache->setEditId(QLatin1String("$153456789:example.org")); QCOMPARE(chatBarCache->text(), QLatin1String("some text")); diff --git a/src/chatbarcache.cpp b/src/chatbarcache.cpp index f027ec67e..66f7cdda3 100644 --- a/src/chatbarcache.cpp +++ b/src/chatbarcache.cpp @@ -139,8 +139,8 @@ void ChatBarCache::setThreadId(const QString &threadId) if (m_threadId == threadId) { return; } - m_threadId = threadId; - Q_EMIT threadIdChanged(); + const auto oldThreadId = std::exchange(m_threadId, threadId); + Q_EMIT threadIdChanged(oldThreadId, m_threadId); } QString ChatBarCache::attachmentPath() const @@ -163,10 +163,10 @@ void ChatBarCache::setAttachmentPath(const QString &attachmentPath) void ChatBarCache::clearRelations() { const auto oldEventId = std::exchange(m_relationId, QString()); - m_threadId = QString(); + const auto oldThreadId = std::exchange(m_threadId, QString()); m_attachmentPath = QString(); Q_EMIT relationIdChanged(oldEventId, m_relationId); - Q_EMIT threadIdChanged(); + Q_EMIT threadIdChanged(oldThreadId, m_threadId); Q_EMIT attachmentPathChanged(); } diff --git a/src/chatbarcache.h b/src/chatbarcache.h index 02304711b..7278af5b3 100644 --- a/src/chatbarcache.h +++ b/src/chatbarcache.h @@ -195,7 +195,7 @@ public: Q_SIGNALS: void textChanged(); void relationIdChanged(const QString &oldEventId, const QString &newEventId); - void threadIdChanged(); + void threadIdChanged(const QString &oldThreadId, const QString &newThreadId); void attachmentPathChanged(); private: diff --git a/src/enums/messagecomponenttype.h b/src/enums/messagecomponenttype.h index d4a406504..548d74956 100644 --- a/src/enums/messagecomponenttype.h +++ b/src/enums/messagecomponenttype.h @@ -50,7 +50,7 @@ public: Reply, /**< A component to show a replied-to message. */ 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. */ + ChatBar, /**< A text edit for editing a message. */ Verification, /**< A user verification session start message. */ Loading, /**< The component is loading. */ Other, /**< Anything that cannot be classified as another type. */ diff --git a/src/models/messagecontentmodel.cpp b/src/models/messagecontentmodel.cpp index 1647e04fd..8d399cb46 100644 --- a/src/models/messagecontentmodel.cpp +++ b/src/models/messagecontentmodel.cpp @@ -140,6 +140,13 @@ void MessageContentModel::initializeModel() endResetModel(); } }); + connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) { + if (m_event != nullptr && (oldThreadId == m_eventId || newThreadId == m_eventId)) { + beginResetModel(); + resetContent(false, newThreadId == m_eventId); + endResetModel(); + } + }); connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() { resetContent(); }); @@ -184,12 +191,7 @@ void MessageContentModel::intiializeEvent(const Quotient::RoomEvent *event) { m_event = loadEvent(event->fullJson()); // a pending event may not previously have had an event ID so update. - if (m_eventId.isEmpty()) { - m_eventId = m_event->id(); - if (m_eventId.isEmpty()) { - m_eventId = m_event->transactionId(); - } - } + m_eventId = EventHandler::id(m_event.get()); auto senderId = m_event->senderId(); // A pending event might not have a sender ID set yet but in that case it must @@ -341,6 +343,12 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const 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 {}; } @@ -372,6 +380,7 @@ QHash MessageContentModel::roleNames() const roles[ReplyAuthorRole] = "replyAuthor"; roles[ReplyContentModelRole] = "replyContentModel"; roles[LinkPreviewerRole] = "linkPreviewer"; + roles[ChatBarCacheRole] = "chatBarCache"; return roles; } @@ -400,7 +409,7 @@ void MessageContentModel::resetModel() endResetModel(); } -void MessageContentModel::resetContent(bool isEditing) +void MessageContentModel::resetContent(bool isEditing, bool isThreading) { Q_ASSERT(m_event != nullptr); @@ -409,7 +418,7 @@ void MessageContentModel::resetContent(bool isEditing) m_components.remove(startRow, rowCount() - startRow); endRemoveRows(); - const auto newComponents = messageContentComponents(isEditing); + const auto newComponents = messageContentComponents(isEditing, isThreading); if (newComponents.size() == 0) { return; } @@ -418,7 +427,7 @@ void MessageContentModel::resetContent(bool isEditing) endInsertRows(); } -QList MessageContentModel::messageContentComponents(bool isEditing) +QList MessageContentModel::messageContentComponents(bool isEditing, bool isThreading) { QList newComponents; @@ -438,7 +447,7 @@ QList MessageContentModel::messageContentComponents(bool isEdi } if (isEditing) { - newComponents += MessageComponent{MessageComponentType::Edit, QString(), {}}; + newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}}; } else { newComponents.append(componentsForType(MessageComponentType::typeForEvent(*m_event.get()))); } @@ -447,6 +456,11 @@ QList MessageContentModel::messageContentComponents(bool isEdi newComponents = addLinkPreviews(newComponents); } + // If the event is already threaded the ThreadModel will handle displaying a chat bar. + if (isThreading && !EventHandler::isThreaded(m_event.get())) { + newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}}; + } + return newComponents; } diff --git a/src/models/messagecontentmodel.h b/src/models/messagecontentmodel.h index 70f7c9bb8..2bc8af975 100644 --- a/src/models/messagecontentmodel.h +++ b/src/models/messagecontentmodel.h @@ -70,6 +70,7 @@ public: ReplyContentModelRole, /**< The MessageContentModel for the reply event. */ LinkPreviewerRole, /**< The link preview details. */ + ChatBarCacheRole, /**< The ChatBarCache to use. */ }; Q_ENUM(Roles) @@ -133,8 +134,8 @@ private: QList m_components; void resetModel(); - void resetContent(bool isEditing = false); - QList messageContentComponents(bool isEditing = false); + void resetContent(bool isEditing = false, bool isThreading = false); + QList messageContentComponents(bool isEditing = false, bool isThreading = false); QPointer m_replyModel; void updateReplyModel(); diff --git a/src/models/threadmodel.cpp b/src/models/threadmodel.cpp index 0fc3e9f0b..e5100c2c3 100644 --- a/src/models/threadmodel.cpp +++ b/src/models/threadmodel.cpp @@ -9,12 +9,15 @@ #include #include +#include "chatbarcache.h" #include "eventhandler.h" +#include "messagecomponenttype.h" #include "neochatroom.h" ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room) : QConcatenateTablesProxyModel(room) , m_threadRootId(threadRootId) + , m_threadChatBarModel(new ThreadChatBarModel(this, room)) { Q_ASSERT(!m_threadRootId.isEmpty()); Q_ASSERT(room); @@ -25,7 +28,6 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room) if (auto roomEvent = eventCast(event)) { if (EventHandler::isThreaded(roomEvent) && EventHandler::threadRoot(roomEvent) == m_threadRootId) { addNewEvent(event); - clearModels(); addModels(); } } @@ -38,7 +40,6 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room) } } } - clearModels(); addModels(); }); @@ -46,6 +47,11 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room) addModels(); } +QString ThreadModel::threadRootId() const +{ + return m_threadRootId; +} + MessageContentModel *ThreadModel::threadRootContentModel() const { return m_threadRootContentModel.get(); @@ -77,7 +83,6 @@ void ThreadModel::fetchMore(const QModelIndex &parent) m_contentModels.push_back(new MessageContentModel(room, event.get())); } - clearModels(); addModels(); const auto newNextBatch = m_currentJob->nextBatch(); @@ -103,13 +108,15 @@ void ThreadModel::addNewEvent(const Quotient::RoomEvent *event) void ThreadModel::addModels() { + if (!sourceModels().isEmpty()) { + clearModels(); + } + addSourceModel(m_threadRootContentModel.get()); for (auto it = m_contentModels.crbegin(); it != m_contentModels.crend(); ++it) { addSourceModel(*it); } - - beginResetModel(); - endResetModel(); + addSourceModel(m_threadChatBarModel); } void ThreadModel::clearModels() @@ -120,6 +127,61 @@ void ThreadModel::clearModels() removeSourceModel(model); } } + removeSourceModel(m_threadChatBarModel); +} + +ThreadChatBarModel::ThreadChatBarModel(QObject *parent, NeoChatRoom *room) + : QAbstractListModel(parent) + , m_room(room) +{ + if (m_room != nullptr) { + connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) { + const auto threadModel = dynamic_cast(this->parent()); + if (threadModel != nullptr && (oldThreadId == threadModel->threadRootId() || newThreadId == threadModel->threadRootId())) { + beginResetModel(); + endResetModel(); + } + }); + } +} + +QVariant ThreadChatBarModel::data(const QModelIndex &idx, int role) const +{ + if (idx.row() > 1) { + return {}; + } + + if (role == ComponentTypeRole) { + return MessageComponentType::ChatBar; + } + if (role == ChatBarCacheRole) { + if (m_room == nullptr) { + return {}; + } + return QVariant::fromValue(m_room->threadCache()); + } + return {}; +} + +int ThreadChatBarModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + if (m_room == nullptr) { + return 0; + } + const auto threadModel = dynamic_cast(this->parent()); + if (threadModel != nullptr) { + return m_room->threadCache()->threadId() == threadModel->threadRootId() ? 1 : 0; + } + return 0; +} + +QHash ThreadChatBarModel::roleNames() const +{ + return { + {ComponentTypeRole, "componentType"}, + {ChatBarCacheRole, "chatBarCache"}, + }; } #include "moc_threadmodel.cpp" diff --git a/src/models/threadmodel.h b/src/models/threadmodel.h index 7ff4f95a8..3f6b87005 100644 --- a/src/models/threadmodel.h +++ b/src/models/threadmodel.h @@ -21,6 +21,57 @@ class NeoChatRoom; class ReactionModel; +/** + * @class ThreadChatBarModel + * + * A model to provide a chat bar component to send new messages in a thread. + */ +class ThreadChatBarModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + +public: + /** + * @brief Defines the model roles. + * + * The role values need to match MessageContentModel not to blow up. + * + * @sa MessageContentModel + */ + enum Roles { + ComponentTypeRole = MessageContentModel::ComponentTypeRole, /**< The type of component to visualise the message. */ + ChatBarCacheRole = MessageContentModel::ChatBarCacheRole, /**< The ChatBarCache to use. */ + }; + Q_ENUM(Roles) + + explicit ThreadChatBarModel(QObject *parent, NeoChatRoom *room); + + /** + * @brief Get the given role value at the given index. + * + * @sa QAbstractItemModel::data + */ + [[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + /** + * @brief 1 or 0, depending on whether a chat bar should be shown. + * + * @sa QAbstractItemModel::rowCount + */ + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + /** + * @brief Returns a map with ComponentTypeRole it's the only one. + * + * @sa Roles, QAbstractItemModel::roleNames() + */ + [[nodiscard]] QHash roleNames() const override; + +private: + QPointer m_room; +}; + /** * @class ThreadModel * @@ -38,6 +89,8 @@ class ThreadModel : public QConcatenateTablesProxyModel public: explicit ThreadModel(const QString &threadRootId, NeoChatRoom *room); + QString threadRootId() const; + /** * @brief The content model for the thread root event. */ @@ -70,6 +123,7 @@ private: std::unique_ptr m_threadRootContentModel; std::deque m_contentModels; + ThreadChatBarModel *m_threadChatBarModel; QList m_events; QList m_pendingEvents; diff --git a/src/qml/HoverActions.qml b/src/qml/HoverActions.qml index 80accbb83..a2fd20cac 100644 --- a/src/qml/HoverActions.qml +++ b/src/qml/HoverActions.qml @@ -123,9 +123,10 @@ QQC2.Control { text: i18n("Reply in Thread") icon.name: "dialog-messages" onTriggered: { - root.currentRoom.mainCache.replyId = ""; - root.currentRoom.mainCache.threadId = root.delegate.isThreaded ? root.delegate.threadRoot : root.delegate.eventId; - root.currentRoom.editCache.editId = ""; + root.currentRoom.threadCache.replyId = ""; + root.currentRoom.threadCache.threadId = root.delegate.isThreaded ? root.delegate.threadRoot : root.delegate.eventId; + root.currentRoom.mainCache.clearRelations(); + root.currentRoom.editCache.clearRelations(); root.focusChatBar(); } } diff --git a/src/timeline/ChatBarComponent.qml b/src/timeline/ChatBarComponent.qml index 89e8dcbaf..604ac91c5 100644 --- a/src/timeline/ChatBarComponent.qml +++ b/src/timeline/ChatBarComponent.qml @@ -118,7 +118,7 @@ QQC2.TextArea { text: i18nc("@action:button", "Cancel") icon.name: "dialog-close" onTriggered: { - root.chatBarCache.editId = ""; + root.chatBarCache.clearRelations(); } shortcut: "Escape" } diff --git a/src/timeline/MessageComponentChooser.qml b/src/timeline/MessageComponentChooser.qml index 0bc7cf081..54e6e821e 100644 --- a/src/timeline/MessageComponentChooser.qml +++ b/src/timeline/MessageComponentChooser.qml @@ -205,10 +205,9 @@ DelegateChooser { } DelegateChoice { - roleValue: MessageComponentType.Edit + roleValue: MessageComponentType.ChatBar delegate: ChatBarComponent { room: root.room - chatBarCache: room.editCache actionsHandler: root.actionsHandler maxContentWidth: root.maxContentWidth }