Loading and End of Timeline Delegates

Add delegate for showing the user a loading indicator and for the beginning of the timeline.

BUG: 455045
BUG: 465285
This commit is contained in:
James Graham
2023-11-20 17:10:56 +00:00
parent 0dbef58ff2
commit 5efd17d370
15 changed files with 370 additions and 41 deletions

View File

@@ -389,13 +389,7 @@ int MessageEventModel::rowCount(const QModelIndex &parent) const
return 0;
}
const auto firstIt = m_currentRoom->messageEvents().crbegin();
if (firstIt != m_currentRoom->messageEvents().crend()) {
const auto &firstEvt = **firstIt;
return m_currentRoom->timelineSize() + (lastReadEventId != firstEvt.id() ? 1 : 0);
} else {
return m_currentRoom->timelineSize();
}
return int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize() + (m_lastReadEventIndex.isValid() ? 1 : 0);
}
bool MessageEventModel::canFetchMore(const QModelIndex &parent) const
@@ -422,7 +416,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
const auto row = idx.row();
if (!m_currentRoom || row < 0 || row >= int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize()) {
if (!m_currentRoom || row < 0 || row >= rowCount()) {
return {};
};
@@ -465,7 +459,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
}
return eventHandler.getRichBody();
}

View File

@@ -8,14 +8,15 @@
#include "enums/delegatetype.h"
#include "messageeventmodel.h"
#include "neochatconfig.h"
#include "timelinemodel.h"
using namespace Quotient;
MessageFilterModel::MessageFilterModel(QObject *parent, MessageEventModel *sourceMessageModel)
MessageFilterModel::MessageFilterModel(QObject *parent, TimelineModel *sourceModel)
: QSortFilterProxyModel(parent)
{
Q_ASSERT(sourceMessageModel);
setSourceModel(sourceMessageModel);
Q_ASSERT(sourceModel);
setSourceModel(sourceModel);
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
invalidateFilter();

View File

@@ -7,6 +7,7 @@
#include <QSortFilterProxyModel>
#include "messageeventmodel.h"
#include "timelinemodel.h"
/**
* @class MessageFilterModel
@@ -36,7 +37,7 @@ public:
LastRole, // Keep this last
};
explicit MessageFilterModel(QObject *parent = nullptr, MessageEventModel *sourceMessageModel = nullptr);
explicit MessageFilterModel(QObject *parent = nullptr, TimelineModel *sourceModel = nullptr);
/**
* @brief Custom filter function to remove hidden messages.

View File

@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "timelinemodel.h"
#include "delegatetype.h"
TimelineModel::TimelineModel(QObject *parent)
: QConcatenateTablesProxyModel(parent)
{
m_messageEventModel = new MessageEventModel(this);
addSourceModel(m_messageEventModel);
m_timelineEndModel = new TimelineEndModel(this);
addSourceModel(m_timelineEndModel);
}
NeoChatRoom *TimelineModel::room() const
{
return m_messageEventModel->room();
}
void TimelineModel::setRoom(NeoChatRoom *room)
{
// Both models do their own null checking so just pass along.
m_messageEventModel->setRoom(room);
m_timelineEndModel->setRoom(room);
}
MessageEventModel *TimelineModel::messageEventModel() const
{
return m_messageEventModel;
}
QHash<int, QByteArray> TimelineModel::roleNames() const
{
return m_messageEventModel->roleNames();
}
TimelineEndModel::TimelineEndModel(QObject *parent)
: QAbstractListModel(parent)
{
}
void TimelineEndModel::setRoom(NeoChatRoom *room)
{
if (room == m_room) {
return;
}
beginResetModel();
if (m_room != nullptr) {
m_room->disconnect(this);
}
m_room = room;
if (m_room != nullptr) {
connect(m_room, &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();
}
});
}
endResetModel();
}
QVariant TimelineEndModel::data(const QModelIndex &idx, int role) const
{
Q_UNUSED(idx)
if (m_room == nullptr) {
return {};
}
if (role == DelegateTypeRole) {
return m_room->allHistoryLoaded() ? DelegateType::TimelineEnd : DelegateType::Loading;
}
return {};
}
int TimelineEndModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 1;
}
QHash<int, QByteArray> TimelineEndModel::roleNames() const
{
return {{DelegateTypeRole, "delegateType"}};
}

112
src/models/timelinemodel.h Normal file
View File

@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QConcatenateTablesProxyModel>
#include <QQmlEngine>
#include "messageeventmodel.h"
#include "neochatroom.h"
/**
* @class TimelineEndModel
*
* A model to provide a single delegate to mark the end of the timeline.
*
* The delegate will either be a loading delegate if more events are being loaded
* or a timeline end delegate if all history is loaded.
*/
class TimelineEndModel : 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 TimelineEndModel(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<int, QByteArray> roleNames() const override;
private:
NeoChatRoom *m_room = nullptr;
};
/**
* @class TimelineModel
*
* A model to visualise a room timeline.
*
* This model combines a MessageEventModel with a TimelineEndModel.
*
* @sa MessageEventModel, TimelineEndModel
*/
class TimelineModel : public QConcatenateTablesProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current room that the model is getting its messages from.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The MessageEventModel for the timeline.
*/
Q_PROPERTY(MessageEventModel *messageEventModel READ messageEventModel CONSTANT)
public:
TimelineModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
MessageEventModel *messageEventModel() const;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_SIGNALS:
void roomChanged();
private:
MessageEventModel *m_messageEventModel = nullptr;
TimelineEndModel *m_timelineEndModel = nullptr;
};