diff --git a/include/libqmatrixclient b/include/libqmatrixclient index 9875149..8bd8aaf 160000 --- a/include/libqmatrixclient +++ b/include/libqmatrixclient @@ -1 +1 @@ -Subproject commit 98751495f1990dccf285e3b4739f86de7b7f68fd +Subproject commit 8bd8aaf0858bb0a0ebcac8c3d29cfbb20279164d diff --git a/qml/component/MessageDelegate.qml b/qml/component/MessageDelegate.qml index defc0e5..e0515de 100644 --- a/qml/component/MessageDelegate.qml +++ b/qml/component/MessageDelegate.qml @@ -1,6 +1,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.2 +import Matrique 0.1 Item { id: messageDelegate @@ -13,7 +14,7 @@ Item { readonly property bool isMessage: eventType === "message" || eventType === "notice" readonly property bool isFile: eventType === "video" || eventType === "audio" || eventType === "file" || eventType === "image" - visible: eventType != "redaction" + visible: marks !== EventStatus.Hidden z: -5 width: delegateLoader.width diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp index 22f813c..0689224 100644 --- a/src/messageeventmodel.cpp +++ b/src/messageeventmodel.cpp @@ -1,7 +1,6 @@ #include "messageeventmodel.h" #include -#include #include // for qmlRegisterType() #include @@ -24,22 +23,27 @@ QHash MessageEventModel::roleNames() const { roles[AboveAuthorRole] = "aboveAuthor"; roles[ContentRole] = "content"; roles[ContentTypeRole] = "contentType"; + roles[HighlightRole] = "highlight"; roles[ReadMarkerRole] = "readMarker"; roles[SpecialMarksRole] = "marks"; roles[LongOperationRole] = "progressInfo"; + roles[AnnotationRole] = "annotation"; roles[EventResolvedTypeRole] = "eventResolvedType"; roles[PlainTextRole] = "plainText"; return roles; } +MessageEventModel::~MessageEventModel() {} + MessageEventModel::MessageEventModel(QObject* parent) : QAbstractListModel(parent), m_currentRoom(nullptr) { - qmlRegisterType(); - qRegisterMetaType(); + using namespace QMatrixClient; + qmlRegisterType(); + qRegisterMetaType(); + qmlRegisterUncreatableType( + "Matrique", 0, 1, "EventStatus", "EventStatus is not an creatable type"); } -MessageEventModel::~MessageEventModel() {} - void MessageEventModel::setRoom(QMatrixClient::Room* room) { if (room == m_currentRoom) return; @@ -52,6 +56,7 @@ void MessageEventModel::setRoom(QMatrixClient::Room* room) { m_currentRoom = room; if (room) { lastReadEventId = room->readMarkerEventId(); + using namespace QMatrixClient; connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [=](RoomEventsRange events) { @@ -61,17 +66,16 @@ void MessageEventModel::setRoom(QMatrixClient::Room* room) { }); connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [=](RoomEventsRange events) { - if (rowCount() > 0) nextNewerRow = rowCount() - 1; // See #312 + if (rowCount() > 0) + rowBelowInserted = rowCount() - 1; // See #312 beginInsertRows(QModelIndex(), rowCount(), rowCount() + int(events.size()) - 1); }); connect(m_currentRoom, &Room::addedMessages, this, [=] { - if (nextNewerRow > -1) { - const auto idx = index(nextNewerRow); - emit dataChanged(idx, idx); - nextNewerRow = -1; - } endInsertRows(); + if (rowBelowInserted > -1) + refreshEventRoles(rowBelowInserted, + {AboveAuthorRole, AboveSectionRole}); }); connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this] { beginInsertRows({}, 0, 0); }); @@ -79,27 +83,38 @@ void MessageEventModel::setRoom(QMatrixClient::Room* room) { &MessageEventModel::endInsertRows); connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this](RoomEvent*, int i) { - const auto timelineBaseIdx = - int(m_currentRoom->pendingEvents().size()); - if (i + 1 == timelineBaseIdx) return; // No need to move anything - mergingEcho = true; - Q_ASSERT(beginMoveRows({}, i, i, {}, timelineBaseIdx)); + if (i == 0) return; // No need to move anything, just refresh + + movingEvent = true; + // Reverse i because row 0 is bottommost in the model + const auto row = timelineBaseIndex() - i - 1; + Q_ASSERT(beginMoveRows({}, row, row, {}, timelineBaseIndex())); }); connect(m_currentRoom, &Room::pendingEventMerged, this, [this] { - if (mergingEcho) { + if (movingEvent) { endMoveRows(); - mergingEcho = false; + movingEvent = false; } - refreshEventRoles(int(m_currentRoom->pendingEvents().size()), - {SpecialMarksRole}); + refreshRow(timelineBaseIndex()); // Refresh the looks + if (m_currentRoom->timelineSize() > 1) // Refresh above + refreshEventRoles(timelineBaseIndex() + 1/*, + {ReadMarkerRole}*/); + if (timelineBaseIndex() > 0) // Refresh below, see #312 + refreshEventRoles(timelineBaseIndex() - 1, + {AboveAuthorRole, AboveSectionRole}); }); connect(m_currentRoom, &Room::pendingEventChanged, this, - [this](int i) { refreshEventRoles(i, {SpecialMarksRole}); }); + &MessageEventModel::refreshRow); + connect(m_currentRoom, &Room::pendingEventAboutToDiscard, this, + [this](int i) { beginRemoveRows({}, i, i); }); + connect(m_currentRoom, &Room::pendingEventDiscarded, this, + &MessageEventModel::endRemoveRows); connect(m_currentRoom, &Room::readMarkerMoved, this, [this] { refreshEventRoles( - std::exchange(lastReadEventId, m_currentRoom->readMarkerEventId()), - {ReadMarkerRole}); - refreshEventRoles(lastReadEventId, {ReadMarkerRole}); + std::exchange(lastReadEventId, + m_currentRoom->readMarkerEventId())/*, + {ReadMarkerRole}*/); + refreshEventRoles(lastReadEventId /*, {ReadMarkerRole}*/); }); connect( m_currentRoom, &Room::replacedEvent, this, @@ -120,11 +135,16 @@ void MessageEventModel::setRoom(QMatrixClient::Room* room) { } void MessageEventModel::refreshEvent(const QString& eventId) { - refreshEventRoles(eventId, {}); + refreshEventRoles(eventId); } -void MessageEventModel::refreshEventRoles(const int row, - const QVector& roles) { +void MessageEventModel::refreshRow(int row) { refreshEventRoles(row); } + +int MessageEventModel::timelineBaseIndex() const { + return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0; +} + +void MessageEventModel::refreshEventRoles(int row, const QVector& roles) { const auto idx = index(row); emit dataChanged(idx, idx, roles); } @@ -132,8 +152,13 @@ void MessageEventModel::refreshEventRoles(const int row, void MessageEventModel::refreshEventRoles(const QString& eventId, const QVector& roles) { const auto it = m_currentRoom->findInTimeline(eventId); - if (it != m_currentRoom->timelineEdge()) - refreshEventRoles(it - m_currentRoom->messageEvents().rbegin(), roles); + if (it == m_currentRoom->timelineEdge()) { + qWarning() << "Trying to refresh inexistent event:" << eventId; + return; + } + refreshEventRoles( + it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex(), + roles); } inline bool hasValidTimestamp(const QMatrixClient::TimelineItem& ti) { @@ -161,9 +186,8 @@ QDateTime MessageEventModel::makeMessageTimestamp( return {}; } -QString MessageEventModel::makeDateString( - const QMatrixClient::Room::rev_iter_t& baseIt) const { - auto date = makeMessageTimestamp(baseIt).toLocalTime().date(); +QString MessageEventModel::renderDate(QDateTime timestamp) const { + auto date = timestamp.toLocalTime().date(); if (QMatrixClient::SettingsGroup("UI") .value("banner_human_friendly_date", true) .toBool()) { @@ -181,20 +205,20 @@ int MessageEventModel::rowCount(const QModelIndex& parent) const { return m_currentRoom->timelineSize(); } -QVariant MessageEventModel::data(const QModelIndex& index, int role) const { - const auto row = index.row(); +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()) return {}; - const auto timelineBaseIdx = int(m_currentRoom->pendingEvents().size()); + bool isPending = row < timelineBaseIndex(); const auto timelineIt = m_currentRoom->messageEvents().crbegin() + - std::max(-1, row - timelineBaseIdx); - const auto& evt = row < timelineBaseIdx - ? *m_currentRoom->pendingEvents()[size_t(row)] - : *timelineIt->event(); + std::max(0, row - timelineBaseIndex()); + const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + + std::min(row, timelineBaseIndex()); + const auto& evt = isPending ? *pendingIt->event() : *timelineIt->event(); using namespace QMatrixClient; if (role == Qt::DisplayRole) { @@ -399,6 +423,7 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const { tr("Unknown Event")); } + if (role == Qt::ToolTipRole) { return evt.originalJson(); } @@ -412,15 +437,10 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const { return "notice"; case MessageEventType::Image: return "image"; - case MessageEventType::File: - case MessageEventType::Audio: - case MessageEventType::Video: - return "file"; default: - return "message"; + return e->hasFileContent() ? "file" : "message"; } } - if (is(evt)) return "redaction"; if (evt.isStateEvent()) return "state"; return "other"; @@ -431,9 +451,8 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const { if (role == AuthorRole) { // FIXME: It shouldn't be User, it should be its state "as of event" - return QVariant::fromValue(row < timelineBaseIdx - ? m_currentRoom->localUser() - : m_currentRoom->user(evt.senderId())); + return QVariant::fromValue(isPending ? m_currentRoom->localUser() + : m_currentRoom->user(evt.senderId())); } if (role == ContentTypeRole) { @@ -466,16 +485,20 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const { if (role == ReadMarkerRole) return evt.id() == lastReadEventId; if (role == SpecialMarksRole) { - if (row < timelineBaseIdx) - return evt.id().isEmpty() ? "unsent" : "unsynced"; + if (isPending) return pendingIt->deliveryStatus(); if (evt.isStateEvent() && - static_cast(evt).repeatsState()) - return "noop"; - return evt.isRedacted() ? "redacted" : ""; + static_cast(evt).repeatsState() && + !Settings().value("UI/show_noop_events", false).toBool()) + return EventStatus::Hidden; + + if (is(evt)) return EventStatus::Hidden; + + return evt.isRedacted() ? EventStatus::Redacted : EventStatus::Normal; } - if (role == EventIdRole) return evt.id(); + if (role == EventIdRole) + return !evt.id().isEmpty() ? evt.id() : evt.transactionId(); if (role == LongOperationRole) { if (auto e = eventCast(&evt)) @@ -483,30 +506,21 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const { return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id())); } - if (row >= timelineBaseIdx - 1) // The timeline and the topmost unsynced - { - if (role == TimeRole) - return row < timelineBaseIdx ? QDateTime::currentDateTimeUtc() - : makeMessageTimestamp(timelineIt); + if (role == AnnotationRole) + if (isPending) return pendingIt->annotation(); - if (role == SectionRole) - return row < timelineBaseIdx - ? tr("Today") - : makeDateString( - timelineIt); // FIXME: move date rendering to QML - - // FIXME: shouldn't be here, because #312 - auto aboveEventIt = timelineIt + 1; - if (aboveEventIt != m_currentRoom->timelineEdge()) { - if (role == AboveSectionRole) return makeDateString(aboveEventIt); - - if (role == AboveAuthorRole) - return QVariant::fromValue( - m_currentRoom->user((*aboveEventIt)->senderId())); - - if (role == AboveTimeRole) return makeMessageTimestamp(aboveEventIt); - } + if (role == TimeRole || role == SectionRole) { + auto ts = + isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt); + return role == TimeRole ? QVariant(ts) : renderDate(ts); } - return QVariant(); + if (role == AboveSectionRole || role == AboveAuthorRole || role == AboveTimeRole) + for (auto r = row + 1; r < rowCount(); ++r) { + auto i = index(r); + if (data(i, SpecialMarksRole) != EventStatus::Hidden) + return data(i, role == AboveSectionRole ? SectionRole : role == AboveAuthorRole ? AuthorRole : TimeRole); + } + + return {}; } diff --git a/src/messageeventmodel.h b/src/messageeventmodel.h index c79a6c8..2ab5b85 100644 --- a/src/messageeventmodel.h +++ b/src/messageeventmodel.h @@ -25,6 +25,7 @@ class MessageEventModel : public QAbstractListModel { ReadMarkerRole, SpecialMarksRole, LongOperationRole, + AnnotationRole, PlainTextRole, // For debugging EventResolvedTypeRole, @@ -36,25 +37,27 @@ class MessageEventModel : public QAbstractListModel { QMatrixClient::Room* getRoom() { return m_currentRoom; } void setRoom(QMatrixClient::Room* room); - Q_INVOKABLE int rowCount( - const QModelIndex& parent = QModelIndex()) const override; - QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QHash roleNames() const; private slots: void refreshEvent(const QString& eventId); + void refreshRow(int row); private: QMatrixClient::Room* m_currentRoom = nullptr; QString lastReadEventId; - bool mergingEcho = 0; - int nextNewerRow = -1; + int rowBelowInserted = -1; + bool movingEvent = 0; + int timelineBaseIndex() const; QDateTime makeMessageTimestamp( const QMatrixClient::Room::rev_iter_t& baseIt) const; - QString makeDateString(const QMatrixClient::Room::rev_iter_t& baseIt) const; - void refreshEventRoles(const int row, const QVector& roles); - void refreshEventRoles(const QString& eventId, const QVector& roles); + QString renderDate(QDateTime timestamp) const; + void refreshEventRoles(int row, const QVector& roles = {}); + void refreshEventRoles(const QString& eventId, + const QVector& roles = {}); signals: void roomChanged();