Refactor TimelineView
Refactor TimelineView to make it more reliable and prepare for read marker choice. This is done by creating signalling from the mode when reset which can be used to move the scrollbar to the newest meassage. Some of the spaghetti is also removed so there is no need for ChatBar and TimelineView to talk directly. The code to mark messages as read if they are all visible after 10s has been removed infour of just marking as read on entry if all are visible. This is temporary until a follow up providing user options is finished (although it will be one of the options)
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
#include <QVariant>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "timelinemessagemodel.h"
|
||||
#include "messagemodel.h"
|
||||
#include "models/timelinemodel.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -19,6 +20,37 @@ MessageFilterModel::MessageFilterModel(QObject *parent, QAbstractItemModel *sour
|
||||
{
|
||||
Q_ASSERT(sourceModel);
|
||||
setSourceModel(sourceModel);
|
||||
|
||||
if (auto model = dynamic_cast<TimelineModel *>(sourceModel)) {
|
||||
connect(model->timelineMessageModel(), &MessageModel::readMarkerIndexChanged, this, &MessageFilterModel::readMarkerIndexChanged);
|
||||
}
|
||||
}
|
||||
|
||||
QPersistentModelIndex MessageFilterModel::readMarkerIndex() const
|
||||
{
|
||||
// Check if sourceModel is a message model.
|
||||
auto messageModel = dynamic_cast<MessageModel *>(sourceModel());
|
||||
bool timelineModelIsSource = false;
|
||||
// See if it's a timeline model.
|
||||
if (!messageModel) {
|
||||
if (const auto timelineModel = dynamic_cast<TimelineModel *>(sourceModel())) {
|
||||
messageModel = timelineModel->timelineMessageModel();
|
||||
timelineModelIsSource = true;
|
||||
if (!messageModel) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto eventIndex = messageModel->readMarkerIndex();
|
||||
if (!eventIndex.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (timelineModelIsSource) {
|
||||
eventIndex = dynamic_cast<TimelineModel *>(sourceModel())->mapFromSource(eventIndex);
|
||||
}
|
||||
return mapFromSource(eventIndex);
|
||||
}
|
||||
|
||||
bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
@@ -93,6 +125,33 @@ QHash<int, QByteArray> MessageFilterModel::roleNames() const
|
||||
return roles;
|
||||
}
|
||||
|
||||
QModelIndex MessageFilterModel::indexforEventId(const QString &eventId) const
|
||||
{
|
||||
// Check if sourceModel is a message model.
|
||||
auto messageModel = dynamic_cast<MessageModel *>(sourceModel());
|
||||
bool timelineModelIsSource = false;
|
||||
// See if it's a timeline model.
|
||||
if (!messageModel) {
|
||||
if (const auto timelineModel = dynamic_cast<TimelineModel *>(sourceModel())) {
|
||||
messageModel = timelineModel->timelineMessageModel();
|
||||
timelineModelIsSource = true;
|
||||
if (!messageModel) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto eventIndex = messageModel->indexforEventId(eventId);
|
||||
if (!eventIndex.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (timelineModelIsSource) {
|
||||
eventIndex = dynamic_cast<TimelineModel *>(sourceModel())->mapFromSource(eventIndex);
|
||||
}
|
||||
return mapFromSource(eventIndex);
|
||||
}
|
||||
|
||||
bool MessageFilterModel::showAuthor(QModelIndex index) const
|
||||
{
|
||||
for (auto r = index.row() + 1; r < rowCount(); ++r) {
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
#include <QQmlEngine>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "timelinemessagemodel.h"
|
||||
#include "timelinemodel.h"
|
||||
#include "models/timelinemessagemodel.h"
|
||||
|
||||
/**
|
||||
* @class MessageFilterModel
|
||||
@@ -25,6 +24,11 @@ class MessageFilterModel : public QSortFilterProxyModel
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The model index of the read marker.
|
||||
*/
|
||||
Q_PROPERTY(QPersistentModelIndex readMarkerIndex READ readMarkerIndex NOTIFY readMarkerIndexChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
@@ -37,6 +41,8 @@ public:
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
|
||||
QPersistentModelIndex readMarkerIndex() const;
|
||||
|
||||
explicit MessageFilterModel(QObject *parent = nullptr, QAbstractItemModel *sourceModel = nullptr);
|
||||
|
||||
/**
|
||||
@@ -58,9 +64,20 @@ public:
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Get the QModelIndex the given event ID in the model.
|
||||
*/
|
||||
Q_INVOKABLE QModelIndex indexforEventId(const QString &eventId) const;
|
||||
|
||||
static void setShowAllEvents(bool enabled);
|
||||
static void setShowDeletedMessages(bool enabled);
|
||||
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* @brief Emitted when the reader marker index is changed.
|
||||
*/
|
||||
void readMarkerIndexChanged();
|
||||
|
||||
private:
|
||||
static bool m_showAllEvents;
|
||||
static bool m_showDeletedMessages;
|
||||
|
||||
@@ -42,8 +42,10 @@ MessageModel::MessageModel(QObject *parent)
|
||||
});
|
||||
|
||||
connect(this, &MessageModel::threadsEnabledChanged, this, [this]() {
|
||||
Q_EMIT modelAboutToBeReset();
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
Q_EMIT modelResetComplete();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,6 +62,7 @@ void MessageModel::setRoom(NeoChatRoom *room)
|
||||
|
||||
clearModel();
|
||||
|
||||
Q_EMIT modelAboutToBeReset();
|
||||
beginResetModel();
|
||||
m_room = room;
|
||||
if (m_room != nullptr) {
|
||||
@@ -67,6 +70,7 @@ void MessageModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
Q_EMIT roomChanged();
|
||||
endResetModel();
|
||||
Q_EMIT modelResetComplete();
|
||||
}
|
||||
|
||||
int MessageModel::timelineServerIndex() const
|
||||
@@ -74,6 +78,11 @@ int MessageModel::timelineServerIndex() const
|
||||
return 0;
|
||||
}
|
||||
|
||||
QPersistentModelIndex MessageModel::readMarkerIndex() const
|
||||
{
|
||||
return m_lastReadEventIndex;
|
||||
}
|
||||
|
||||
std::optional<std::reference_wrapper<const Quotient::RoomEvent>> MessageModel::getEventForIndex(QModelIndex index) const
|
||||
{
|
||||
Q_UNUSED(index)
|
||||
@@ -342,18 +351,18 @@ QHash<int, QByteArray> MessageModel::roleNames() const
|
||||
return roles;
|
||||
}
|
||||
|
||||
int MessageModel::eventIdToRow(const QString &eventID) const
|
||||
QModelIndex MessageModel::indexforEventId(const QString &eventId) const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto it = m_room->findInTimeline(eventID);
|
||||
const auto it = m_room->findInTimeline(eventId);
|
||||
if (it == m_room->historyEdge()) {
|
||||
// qWarning() << "Trying to find inexistent event:" << eventID;
|
||||
return -1;
|
||||
qWarning() << "Trying to find non-existent event:" << eventId;
|
||||
return {};
|
||||
}
|
||||
return it - m_room->messageEvents().rbegin() + timelineServerIndex();
|
||||
return index(it - m_room->messageEvents().rbegin() + timelineServerIndex());
|
||||
}
|
||||
|
||||
void MessageModel::fullEventRefresh(int row)
|
||||
@@ -455,6 +464,47 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event)
|
||||
}
|
||||
}
|
||||
|
||||
void MessageModel::moveReadMarker(const QString &toEventId)
|
||||
{
|
||||
const auto timelineIt = m_room->findInTimeline(toEventId);
|
||||
if (timelineIt == m_room->historyEdge()) {
|
||||
return;
|
||||
}
|
||||
int newRow = int(timelineIt - m_room->messageEvents().rbegin()) + timelineServerIndex();
|
||||
|
||||
if (!m_lastReadEventIndex.isValid()) {
|
||||
// Not valid index means we don't display any marker yet, in this case
|
||||
// we create the new index and insert the row in case the read marker
|
||||
// need to be displayed.
|
||||
if (newRow > timelineServerIndex()) {
|
||||
// The user didn't read all the messages yet.
|
||||
beginInsertRows({}, newRow, newRow);
|
||||
m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0));
|
||||
endInsertRows();
|
||||
Q_EMIT readMarkerIndexChanged();
|
||||
Q_EMIT readMarkerAdded();
|
||||
return;
|
||||
}
|
||||
// The user read all the messages and we didn't display any read marker yet
|
||||
// => do nothing
|
||||
return;
|
||||
}
|
||||
if (newRow <= timelineServerIndex()) {
|
||||
// The user read all the messages => remove read marker
|
||||
beginRemoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row());
|
||||
m_lastReadEventIndex = QModelIndex();
|
||||
endRemoveRows();
|
||||
Q_EMIT readMarkerIndexChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// The user didn't read all the messages yet but moved the reader marker.
|
||||
beginMoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row(), {}, newRow);
|
||||
m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0));
|
||||
endMoveRows();
|
||||
Q_EMIT readMarkerIndexChanged();
|
||||
}
|
||||
|
||||
void MessageModel::clearModel()
|
||||
{
|
||||
if (m_room) {
|
||||
@@ -462,10 +512,12 @@ void MessageModel::clearModel()
|
||||
|
||||
// HACK: Reset the model to a null room first to make sure QML dismantles
|
||||
// last room's objects before the room is actually changed
|
||||
Q_EMIT modelAboutToBeReset();
|
||||
beginResetModel();
|
||||
m_room->disconnect(this);
|
||||
m_room = nullptr;
|
||||
endResetModel();
|
||||
Q_EMIT modelResetComplete();
|
||||
|
||||
// Because we don't want any of the object deleted before the model is cleared.
|
||||
oldRoom->setVisible(false);
|
||||
|
||||
@@ -51,6 +51,11 @@ class MessageModel : public QAbstractListModel
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief The model index of the read marker.
|
||||
*/
|
||||
Q_PROPERTY(QPersistentModelIndex readMarkerIndex READ readMarkerIndex NOTIFY readMarkerIndexChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
@@ -94,6 +99,8 @@ public:
|
||||
[[nodiscard]] NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
QPersistentModelIndex readMarkerIndex() const;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
@@ -109,9 +116,9 @@ public:
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Get the row number of the given event ID in the model.
|
||||
* @brief Get the QModelIndex of the given event ID in the model.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
|
||||
Q_INVOKABLE QModelIndex indexforEventId(const QString &eventId) const;
|
||||
|
||||
static void setHiddenFilter(std::function<bool(const Quotient::RoomEvent *)> hiddenFilter);
|
||||
|
||||
@@ -123,6 +130,26 @@ Q_SIGNALS:
|
||||
*/
|
||||
void roomChanged();
|
||||
|
||||
/**
|
||||
* @brief Emitted when the reader marker is added.
|
||||
*/
|
||||
void readMarkerAdded();
|
||||
|
||||
/**
|
||||
* @brief Emitted when the reader marker index is changed.
|
||||
*/
|
||||
void readMarkerIndexChanged();
|
||||
|
||||
/**
|
||||
* @brief Emitted when the model is about to reset.
|
||||
*/
|
||||
void modelAboutToBeReset();
|
||||
|
||||
/**
|
||||
* @brief Emitted when the model has been reset.
|
||||
*/
|
||||
void modelResetComplete();
|
||||
|
||||
/**
|
||||
* @brief A signal to tell the MessageModel that a new event has been added.
|
||||
*
|
||||
@@ -131,6 +158,11 @@ Q_SIGNALS:
|
||||
*/
|
||||
void newEventAdded(const Quotient::RoomEvent *event);
|
||||
|
||||
/**
|
||||
* @brief A signal that should be emitted when the local user posts a new event in the room.
|
||||
*/
|
||||
void newLocalUserEventAdded();
|
||||
|
||||
void threadsEnabledChanged();
|
||||
|
||||
protected:
|
||||
@@ -145,6 +177,8 @@ protected:
|
||||
void refreshEventRoles(int row, const QList<int> &roles = {});
|
||||
void refreshLastUserEvents(int baseTimelineRow);
|
||||
|
||||
void moveReadMarker(const QString &toEventId);
|
||||
|
||||
void clearModel();
|
||||
void clearEventObjects();
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ void TimelineMessageModel::connectNewRoom()
|
||||
connect(m_room, &Room::pendingEventAdded, this, [this](const Quotient::RoomEvent *event) {
|
||||
m_initialized = true;
|
||||
Q_EMIT newEventAdded(event);
|
||||
Q_EMIT newLocalUserEventAdded();
|
||||
beginInsertRows({}, 0, 0);
|
||||
endInsertRows();
|
||||
});
|
||||
@@ -143,44 +144,6 @@ int TimelineMessageModel::timelineServerIndex() const
|
||||
return m_room ? int(m_room->pendingEvents().size()) : 0;
|
||||
}
|
||||
|
||||
void TimelineMessageModel::moveReadMarker(const QString &toEventId)
|
||||
{
|
||||
const auto timelineIt = m_room->findInTimeline(toEventId);
|
||||
if (timelineIt == m_room->historyEdge()) {
|
||||
return;
|
||||
}
|
||||
int newRow = int(timelineIt - m_room->messageEvents().rbegin()) + timelineServerIndex();
|
||||
|
||||
if (!m_lastReadEventIndex.isValid()) {
|
||||
// Not valid index means we don't display any marker yet, in this case
|
||||
// we create the new index and insert the row in case the read marker
|
||||
// need to be displayed.
|
||||
if (newRow > timelineServerIndex()) {
|
||||
// The user didn't read all the messages yet.
|
||||
m_initialized = true;
|
||||
beginInsertRows({}, newRow, newRow);
|
||||
m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0));
|
||||
endInsertRows();
|
||||
return;
|
||||
}
|
||||
// The user read all the messages and we didn't display any read marker yet
|
||||
// => do nothing
|
||||
return;
|
||||
}
|
||||
if (newRow <= timelineServerIndex()) {
|
||||
// The user read all the messages => remove read marker
|
||||
beginRemoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row());
|
||||
m_lastReadEventIndex = QModelIndex();
|
||||
endRemoveRows();
|
||||
return;
|
||||
}
|
||||
|
||||
// The user didn't read all the messages yet but moved the reader marker.
|
||||
beginMoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row(), {}, newRow);
|
||||
m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0));
|
||||
endMoveRows();
|
||||
}
|
||||
|
||||
std::optional<std::reference_wrapper<const RoomEvent>> TimelineMessageModel::getEventForIndex(QModelIndex index) const
|
||||
{
|
||||
const auto row = index.row();
|
||||
|
||||
@@ -41,6 +41,9 @@ public:
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
|
||||
private:
|
||||
void connectNewRoom();
|
||||
|
||||
@@ -52,11 +55,6 @@ private:
|
||||
|
||||
int timelineServerIndex() const override;
|
||||
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
|
||||
void moveReadMarker(const QString &toEventId);
|
||||
|
||||
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
@@ -43,6 +43,22 @@ QHash<int, QByteArray> TimelineModel::roleNames() const
|
||||
return m_timelineMessageModel->roleNames();
|
||||
}
|
||||
|
||||
bool TimelineModel::canFetchMore(const QModelIndex &parent) const
|
||||
{
|
||||
if (!m_timelineMessageModel) {
|
||||
return false;
|
||||
}
|
||||
return m_timelineMessageModel->canFetchMore(parent);
|
||||
}
|
||||
|
||||
void TimelineModel::fetchMore(const QModelIndex &parent)
|
||||
{
|
||||
if (!m_timelineMessageModel) {
|
||||
return;
|
||||
}
|
||||
return m_timelineMessageModel->fetchMore(parent);
|
||||
}
|
||||
|
||||
TimelineBeginningModel::TimelineBeginningModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
|
||||
@@ -162,4 +162,7 @@ private:
|
||||
TimelineMessageModel *m_timelineMessageModel = nullptr;
|
||||
TimelineBeginningModel *m_timelineBeginningModel = nullptr;
|
||||
TimelineEndModel *m_timelineEndModel = nullptr;
|
||||
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user