From b3afa9f5954a2fecd7e5bd15948a7ec509da19c8 Mon Sep 17 00:00:00 2001 From: James Graham Date: Thu, 22 Aug 2024 17:21:36 +0000 Subject: [PATCH] Add delegates to show room upgrades into the timeline model. The delegates are at the beginning for upgraded rooms and end for predecessors. Closes: network/neochat#620 and network/neochat#619 --- src/enums/delegatetype.h | 2 + src/models/timelinemodel.cpp | 79 +++++++++++++++++++++++++++- src/models/timelinemodel.h | 52 ++++++++++++++++++ src/timeline/CMakeLists.txt | 2 + src/timeline/EventDelegate.qml | 14 +++++ src/timeline/PredecessorDelegate.qml | 33 ++++++++++++ src/timeline/SuccessorDelegate.qml | 33 ++++++++++++ 7 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 src/timeline/PredecessorDelegate.qml create mode 100644 src/timeline/SuccessorDelegate.qml diff --git a/src/enums/delegatetype.h b/src/enums/delegatetype.h index 4ddb202cc..06001ab7e 100644 --- a/src/enums/delegatetype.h +++ b/src/enums/delegatetype.h @@ -38,6 +38,8 @@ public: ReadMarker, /**< The local user read marker. */ Loading, /**< A delegate to tell the user more messages are being loaded. */ TimelineEnd, /**< A delegate to inform that all messages are loaded. */ + Predecessor, /**< A delegate to show a room predecessor. */ + Successor, /**< A delegate to show a room successor. */ Other, /**< Anything that cannot be classified as another type. */ }; Q_ENUM(Type); diff --git a/src/models/timelinemodel.cpp b/src/models/timelinemodel.cpp index 7ba76b410..c3a83f224 100644 --- a/src/models/timelinemodel.cpp +++ b/src/models/timelinemodel.cpp @@ -3,11 +3,15 @@ #include "timelinemodel.h" +#include + #include "delegatetype.h" TimelineModel::TimelineModel(QObject *parent) : QConcatenateTablesProxyModel(parent) { + m_timelineBeginningModel = new TimelineBeginningModel(this); + addSourceModel(m_timelineBeginningModel); m_messageEventModel = new MessageEventModel(this); addSourceModel(m_messageEventModel); m_timelineEndModel = new TimelineEndModel(this); @@ -23,6 +27,7 @@ void TimelineModel::setRoom(NeoChatRoom *room) { // Both models do their own null checking so just pass along. m_messageEventModel->setRoom(room); + m_timelineBeginningModel->setRoom(room); m_timelineEndModel->setRoom(room); } @@ -36,6 +41,69 @@ QHash TimelineModel::roleNames() const return m_messageEventModel->roleNames(); } +TimelineBeginningModel::TimelineBeginningModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +void TimelineBeginningModel::setRoom(NeoChatRoom *room) +{ + if (room == m_room) { + return; + } + + beginResetModel(); + + if (m_room != nullptr) { + m_room->disconnect(this); + } + + m_room = room; + + if (m_room != nullptr) { + Quotient::connectUntil(m_room.get(), &Quotient::Room::eventsHistoryJobChanged, this, [this]() { + if (m_room->allHistoryLoaded()) { + // HACK: We have to do it this way because DelegateChooser doesn't update dynamically. + beginRemoveRows({}, 0, 0); + endRemoveRows(); + beginInsertRows({}, 0, 0); + endInsertRows(); + return true; + } + return false; + }); + } + + endResetModel(); +} + +QVariant TimelineBeginningModel::data(const QModelIndex &idx, int role) const +{ + Q_UNUSED(idx) + if (m_room == nullptr) { + return {}; + } + + if (role == DelegateTypeRole) { + return DelegateType::Successor; + } + return {}; +} + +int TimelineBeginningModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + if (m_room == nullptr) { + return 0; + } + return m_room->successorId().isEmpty() ? 0 : 1; +} + +QHash TimelineBeginningModel::roleNames() const +{ + return {{DelegateTypeRole, "delegateType"}}; +} + TimelineEndModel::TimelineEndModel(QObject *parent) : QAbstractListModel(parent) { @@ -78,7 +146,11 @@ QVariant TimelineEndModel::data(const QModelIndex &idx, int role) const } if (role == DelegateTypeRole) { - return m_room->allHistoryLoaded() ? DelegateType::TimelineEnd : DelegateType::Loading; + if (idx.row() == 1 || rowCount() == 1) { + return m_room->allHistoryLoaded() ? DelegateType::TimelineEnd : DelegateType::Loading; + } else { + return DelegateType::Predecessor; + } } return {}; } @@ -86,7 +158,10 @@ QVariant TimelineEndModel::data(const QModelIndex &idx, int role) const int TimelineEndModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) - return 1; + if (m_room == nullptr) { + return 0; + } + return m_room->predecessorId().isEmpty() ? 1 : (m_room->allHistoryLoaded() ? 2 : 1); } QHash TimelineEndModel::roleNames() const diff --git a/src/models/timelinemodel.h b/src/models/timelinemodel.h index 714ee0919..23defeb66 100644 --- a/src/models/timelinemodel.h +++ b/src/models/timelinemodel.h @@ -10,6 +10,57 @@ #include "messageeventmodel.h" #include "neochatroom.h" +/** + * @class TimelineBeginningModel + * + * A model to provide a delegate at the start of the timeline to show upgrades. + */ +class TimelineBeginningModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + +public: + /** + * @brief Defines the model roles. + */ + enum Roles { + DelegateTypeRole = MessageEventModel::DelegateTypeRole, /**< The delegate type of the message. */ + }; + Q_ENUM(Roles) + + explicit TimelineBeginningModel(QObject *parent = nullptr); + + /** + * @brief Set the room for the timeline. + */ + void setRoom(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, the answer is always 1. + * + * @sa QAbstractItemModel::rowCount + */ + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + /** + * @brief Returns a map with DelegateTypeRole it's the only one. + * + * @sa Roles, QAbstractItemModel::roleNames() + */ + [[nodiscard]] QHash roleNames() const override; + +private: + QPointer m_room = nullptr; +}; + /** * @class TimelineEndModel * @@ -108,5 +159,6 @@ Q_SIGNALS: private: MessageEventModel *m_messageEventModel = nullptr; + TimelineBeginningModel *m_timelineBeginningModel = nullptr; TimelineEndModel *m_timelineEndModel = nullptr; }; diff --git a/src/timeline/CMakeLists.txt b/src/timeline/CMakeLists.txt index f1ceb7946..694bd5957 100644 --- a/src/timeline/CMakeLists.txt +++ b/src/timeline/CMakeLists.txt @@ -10,8 +10,10 @@ ecm_add_qml_module(timeline GENERATE_PLUGIN_SOURCE HiddenDelegate.qml MessageDelegate.qml LoadingDelegate.qml + PredecessorDelegate.qml ReadMarkerDelegate.qml StateDelegate.qml + SuccessorDelegate.qml TimelineEndDelegate.qml Bubble.qml AvatarFlow.qml diff --git a/src/timeline/EventDelegate.qml b/src/timeline/EventDelegate.qml index b345833e7..8dd0ea802 100644 --- a/src/timeline/EventDelegate.qml +++ b/src/timeline/EventDelegate.qml @@ -41,6 +41,20 @@ DelegateChooser { delegate: LoadingDelegate {} } + DelegateChoice { + roleValue: DelegateType.Predecessor + delegate: PredecessorDelegate { + room: root.room + } + } + + DelegateChoice { + roleValue: DelegateType.Successor + delegate: SuccessorDelegate { + room: root.room + } + } + DelegateChoice { roleValue: DelegateType.TimelineEnd delegate: TimelineEndDelegate { diff --git a/src/timeline/PredecessorDelegate.qml b/src/timeline/PredecessorDelegate.qml new file mode 100644 index 000000000..7b1695285 --- /dev/null +++ b/src/timeline/PredecessorDelegate.qml @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +import QtQuick +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +import org.kde.neochat + +TimelineDelegate { + id: root + + /** + * @brief The current room that user is viewing. + */ + required property NeoChatRoom room + + width: parent?.width + rightPadding: NeoChatConfig.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing + + alwaysFillWidth: NeoChatConfig.compactLayout + + contentItem: Kirigami.InlineMessage { + visible: true + text: i18n("This room continues another conversation.") + type: Kirigami.MessageType.Information + actions: Kirigami.Action { + text: i18n("See older messages…") + onTriggered: RoomManager.resolveResource(root.room.predecessorId) + } + } +} diff --git a/src/timeline/SuccessorDelegate.qml b/src/timeline/SuccessorDelegate.qml new file mode 100644 index 000000000..202e92cef --- /dev/null +++ b/src/timeline/SuccessorDelegate.qml @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +import QtQuick +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +import org.kde.neochat + +TimelineDelegate { + id: root + + /** + * @brief The current room that user is viewing. + */ + required property NeoChatRoom room + + width: parent?.width + rightPadding: NeoChatConfig.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing + + alwaysFillWidth: NeoChatConfig.compactLayout + + contentItem: Kirigami.InlineMessage { + visible: true + text: i18n("This room has been replaced.") + type: Kirigami.MessageType.Information + actions: Kirigami.Action { + text: i18n("See new room…") + onTriggered: RoomManager.resolveResource(root.room.successorId) + } + } +}