diff --git a/client/models/messageeventmodel.cpp b/client/models/messageeventmodel.cpp index e0d776c91..e36c3a0aa 100644 --- a/client/models/messageeventmodel.cpp +++ b/client/models/messageeventmodel.cpp @@ -46,6 +46,7 @@ enum EventRoles { ReadMarkerRole, SpecialMarksRole, LongOperationRole, + AnnotationRole, // For debugging EventResolvedTypeRole, }; @@ -66,6 +67,7 @@ QHash MessageEventModel::roleNames() const roles[ReadMarkerRole] = "readMarker"; roles[SpecialMarksRole] = "marks"; roles[LongOperationRole] = "progressInfo"; + roles[AnnotationRole] = "annotation"; roles[EventResolvedTypeRole] = "eventResolvedType"; return roles; } @@ -74,8 +76,10 @@ MessageEventModel::MessageEventModel(QObject* parent) : QAbstractListModel(parent) , m_currentRoom(nullptr) { - qmlRegisterType(); - qRegisterMetaType(); + using namespace QMatrixClient; + qmlRegisterType(); qRegisterMetaType(); + qmlRegisterUncreatableType("QMatrixClient", 1, 0, "EventStatus", + "EventStatus is not an creatable type"); } void MessageEventModel::changeRoom(QuaternionRoom* room) @@ -94,6 +98,7 @@ void MessageEventModel::changeRoom(QuaternionRoom* room) if( room ) { lastReadEventId = room->readMarkerEventId(); + using namespace QMatrixClient; connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [=](RoomEventsRange events) @@ -106,19 +111,16 @@ void MessageEventModel::changeRoom(QuaternionRoom* room) [=](RoomEventsRange events) { if (rowCount() > 0) - nextNewerRow = rowCount() - 1; // See #312 + 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); }); @@ -127,31 +129,43 @@ void MessageEventModel::changeRoom(QuaternionRoom* room) 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 }); }); - connect(m_currentRoom, &Room::readMarkerMoved, this, [this] { + connect(m_currentRoom, &Room::pendingEventChanged, + this, &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}); + m_currentRoom->readMarkerEventId())/*, + {ReadMarkerRole}*/); + refreshEventRoles(lastReadEventId/*, {ReadMarkerRole}*/); }); connect(m_currentRoom, &Room::replacedEvent, this, [this] (const RoomEvent* newEvent) { @@ -174,11 +188,20 @@ void MessageEventModel::changeRoom(QuaternionRoom* 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); @@ -188,8 +211,16 @@ 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; + } + if (roles.empty() || roles.contains(ReadMarkerRole)) + qDebug() << "Refreshing" << eventId << ", lREI" << lastReadEventId + << "rMEI" << m_currentRoom->readMarkerEventId(); + refreshEventRoles(it - m_currentRoom->messageEvents().rbegin() + + timelineBaseIndex(), roles); } inline bool hasValidTimestamp(const QMatrixClient::TimelineItem& ti) @@ -221,10 +252,9 @@ QDateTime MessageEventModel::makeMessageTimestamp( return {}; } -QString MessageEventModel::makeDateString( - const QuaternionRoom::rev_iter_t& baseIt) const +QString MessageEventModel::renderDate(QDateTime timestamp) const { - auto date = makeMessageTimestamp(baseIt).toLocalTime().date(); + auto date = timestamp.toLocalTime().date(); if (QMatrixClient::SettingsGroup("UI") .value("banner_human_friendly_date", true).toBool()) { @@ -247,21 +277,21 @@ int MessageEventModel::rowCount(const QModelIndex& parent) const return m_currentRoom->timelineSize(); } -QVariant MessageEventModel::data(const QModelIndex& index, int role) const +QVariant MessageEventModel::data(const QModelIndex& idx, int role) const { - const auto row = index.row(); + 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 ) @@ -422,7 +452,7 @@ 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 + return QVariant::fromValue(isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId())); } @@ -463,21 +493,24 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const return m_currentRoom->isEventHighlighted(&evt); if( role == ReadMarkerRole ) + { + qDebug() << "ReadMarkerRole:" << evt.id() << "vs" << lastReadEventId; 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" : ""; + return EventStatus::Hidden; + return evt.isRedacted() ? EventStatus::Redacted : EventStatus::Normal; } if( role == EventIdRole ) - return evt.id(); + return !evt.id().isEmpty() ? evt.id() : evt.transactionId(); if( role == LongOperationRole ) { @@ -487,28 +520,25 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const 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 == SectionRole ) - return row < timelineBaseIdx ? tr("Today") - : makeDateString(timelineIt); // FIXME: move date rendering to QML + if( role == AnnotationRole ) + if (isPending) + return pendingIt->annotation(); - // 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 == TimeRole || role == SectionRole) + { + auto ts = isPending ? pendingIt->lastUpdated() + : makeMessageTimestamp(timelineIt); + return role == TimeRole ? QVariant(ts) : renderDate(ts); } - return QVariant(); + if (role == AboveSectionRole) + return data(index(row + 1), SectionRole); + + if( role == AboveAuthorRole ) + return isPending + ? QVariant::fromValue(m_currentRoom->localUser()) + : data(index(row + 1), AuthorRole); + + return {}; } diff --git a/client/models/messageeventmodel.h b/client/models/messageeventmodel.h index 390084d93..0fb41490e 100644 --- a/client/models/messageeventmodel.h +++ b/client/models/messageeventmodel.h @@ -37,15 +37,18 @@ class MessageEventModel: public QAbstractListModel private slots: void refreshEvent(const QString& eventId); + void refreshRow(int row); private: QuaternionRoom* m_currentRoom; QString lastReadEventId; - int nextNewerRow = -1; - bool mergingEcho = 0; - - 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); + int rowBelowInserted = -1; + bool movingEvent = 0; + + int timelineBaseIndex() const; + QDateTime makeMessageTimestamp(const QuaternionRoom::rev_iter_t& baseIt) const; + QString renderDate(QDateTime timestamp) const; + void refreshEventRoles(int row, const QVector& roles = {}); + void refreshEventRoles(const QString& eventId, + const QVector& roles = {}); }; diff --git a/client/qml/ActiveLabel.qml b/client/qml/ActiveLabel.qml new file mode 100644 index 000000000..0330b134a --- /dev/null +++ b/client/qml/ActiveLabel.qml @@ -0,0 +1,16 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.4 + +Label { + signal clicked + + font.italic: true + textFormat: Text.PlainText + MouseArea + { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onClicked: parent.clicked + } +} diff --git a/client/qml/TimelineItem.qml b/client/qml/TimelineItem.qml index b5a9dc21b..c3970a1c5 100644 --- a/client/qml/TimelineItem.qml +++ b/client/qml/TimelineItem.qml @@ -29,20 +29,26 @@ Item { // TimelineItem definition visible: eventType != "redaction" && - (marks != "noop" || settings.show_noop_events) + (marks !== EventStatus.Hidden || settings.show_noop_events) height: visible ? childrenRect.height : 0 readonly property bool sectionVisible: section !== aboveSection - readonly property bool redacted: marks == "redacted" + readonly property bool redacted: marks === EventStatus.Redacted + readonly property bool pending: [ + EventStatus.Submitted, + EventStatus.Departed, + EventStatus.ReachedServer, + EventStatus.SendingFailed + ].indexOf(marks) != -1 + readonly property bool failed: marks === EventStatus.SendingFailed readonly property string textColor: - marks == "unsent" ? defaultPalette.mid : - marks == "unsynced" ? disabledPalette.text : + marks === EventStatus.Submitted || failed ? defaultPalette.mid : + marks === EventStatus.Departed ? disabledPalette.text : redacted ? disabledPalette.text : highlight ? settings.highlight_color : (["state", "notice", "other"].indexOf(eventType) >= 0) ? disabledPalette.text : defaultPalette.text - readonly property string authorName: - room.roomMembername(author.id) + readonly property string authorName: room.roomMembername(author.id) readonly property bool xchatStyle: settings.timeline_style === "xchat" readonly property bool actionEvent: eventType == "state" || eventType == "emote" @@ -55,7 +61,7 @@ Item { y + message.height - 1 < view.contentY + view.height onShownChanged: { - if (marks != "unsynced" && marks != "unsent") + if (!pending) controller.onMessageShownChanged(eventId, shown) } @@ -177,6 +183,7 @@ Item { color: disabledPalette.text textFormat: Text.RichText renderType: settings.render_type + font.italic: pending text: "<" + time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) @@ -187,13 +194,14 @@ Item { anchors.top: singleRow ? authorLabel.top : authorLabel.bottom anchors.left: singleRow ? authorLabel.right : timelabel.right anchors.leftMargin: 3 - anchors.right: showDetailsButton.left + anchors.right: resendButton.left anchors.rightMargin: 3 selectByMouse: true readOnly: true textFormat: TextEdit.RichText - text: xchatStyle || !singleRow ? display : ' ' + display + text: ((xchatStyle || !singleRow) ? display : ' ' + display) + + (annotation ? "
" + annotation + "" : "") horizontalAlignment: Text.AlignLeft wrapMode: Text.Wrap color: textColor @@ -247,6 +255,29 @@ Item { sourceComponent: FileContent { } } + ActiveLabel { + id: resendButton + visible: failed + width: visible * implicitWidth + anchors.top: textField.top + anchors.right: discardButton.left + anchors.rightMargin: 2 + text: "Resend" + + onClicked: room.retryMessage(eventId) + } + ActiveLabel { + id: discardButton + visible: pending && marks !== EventStatus.ReachedServer + width: visible * implicitWidth + anchors.top: textField.top + anchors.right: showDetailsButton.left + anchors.rightMargin: 2 + text: "Discard" + + onClicked: room.discardMessage(eventId) + } + ToolButton { id: showDetailsButton anchors.top: textField.top diff --git a/client/resources.qrc b/client/resources.qrc index 83e4933cc..1f8d6a501 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -13,5 +13,6 @@ qml/ImageContent.qml qml/FileContent.qml qml/TimelineItem.qml + qml/ActiveLabel.qml diff --git a/lib b/lib index 84aa055bb..7298e9912 160000 --- a/lib +++ b/lib @@ -1 +1 @@ -Subproject commit 84aa055bba602635599def37915b96ccf3f63484 +Subproject commit 7298e99125522c4d010ec83052cd10ce085e09b4