From 240b3a566b8f73261bd6c48ae7480800136e3ec2 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 18 Sep 2019 22:58:25 +0200 Subject: [PATCH] Add send/received indicator --- resources/qml/StatusIndicator.qml | 44 +++++++++++++++++++++++++++++ resources/qml/TimelineView.qml | 5 ++++ resources/res.qrc | 1 + src/timeline2/TimelineModel.cpp | 43 +++++++++++++++++++++++++++- src/timeline2/TimelineModel.h | 47 +++++++++++++++++++++++++++---- 5 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 resources/qml/StatusIndicator.qml diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml new file mode 100644 index 00000000..0b14e246 --- /dev/null +++ b/resources/qml/StatusIndicator.qml @@ -0,0 +1,44 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.5 +import QtGraphicalEffects 1.0 +import com.github.nheko 1.0 + +Rectangle { + id: indicator + property int state: 0 + color: "transparent" + width: 16 + height: 16 + ToolTip.visible: ma.containsMouse + ToolTip.text: switch (state) { + case MtxEvent.Failed: return qsTr("Failed") + case MtxEvent.Sent: return qsTr("Sent") + case MtxEvent.Received: return qsTr("Received") + case MtxEvent.Read: return qsTr("Read") + default: return qsTr("Empty") + } + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: switch (indicator.state) { + case MtxEvent.Failed: return "qrc:/icons/icons/ui/remove-symbol.png" + case MtxEvent.Sent: return "qrc:/icons/icons/ui/clock.png" + case MtxEvent.Received: return "qrc:/icons/icons/ui/checkmark.png" + case MtxEvent.Read: return "qrc:/icons/icons/ui/double-tick-indicator.png" + default: return "" + } + } + ColorOverlay { + anchors.fill: stateImg + source: stateImg + color: colors.buttonText + } +} + diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index f82cf60a..5eb00b06 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -116,6 +116,11 @@ Rectangle { property variant eventData: model } + StatusIndicator { + state: model.state + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } Button { Layout.alignment: Qt.AlignRight | Qt.AlignTop diff --git a/resources/res.qrc b/resources/res.qrc index 6eb61e3d..a9cf885b 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -117,6 +117,7 @@ qml/TimelineView.qml qml/Avatar.qml + qml/StatusIndicator.qml qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 7a2edda4..13429c3e 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -240,6 +240,35 @@ TimelineModel::TimelineModel(QString room_id, QObject *parent) { connect( this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); + connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { + pending.remove(txn_id); + failed.insert(txn_id); + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Failed index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Sent index out of range"); + return; + } + eventOrder[idx] = event_id; + auto ev = events.value(txn_id); + ev = boost::apply_visitor( + [event_id](const auto &e) -> mtx::events::collections::TimelineEvents { + auto eventCopy = e; + eventCopy.event_id = event_id.toStdString(); + return eventCopy; + }, + ev); + events.remove(txn_id); + events.insert(event_id, ev); + emit dataChanged(index(idx, 0), index(idx, 0)); + }); } QHash @@ -258,6 +287,7 @@ TimelineModel::roleNames() const {Width, "width"}, {ProportionalHeight, "proportionalHeight"}, {Id, "id"}, + {State, "state"}, }; } int @@ -341,6 +371,13 @@ TimelineModel::data(const QModelIndex &index, int role) const [](const auto &e) -> double { return eventPropHeight(e); }, event)); case Id: return id; + case State: + if (failed.contains(id)) + return qml_mtx_events::Failed; + else if (pending.contains(id)) + return qml_mtx_events::Sent; + else + return qml_mtx_events::Received; default: return QVariant(); } @@ -378,8 +415,12 @@ TimelineModel::internalAddEvents( QString id = boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); - if (this->events.contains(id)) + if (this->events.contains(id)) { + this->events.insert(id, e); + int idx = idToIndex(id); + emit dataChanged(index(idx, 0), index(idx, 0)); continue; + } if (auto redaction = boost::get>(&e)) { diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 17f83323..b651708d 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "Logging.h" #include "MatrixClient.h" @@ -68,6 +69,21 @@ enum EventType UnknownMessage, }; Q_ENUM_NS(EventType) + +enum EventState +{ + //! The plaintext message was received by the server. + Received, + //! At least one of the participants has read the message. + Read, + //! The client sent the message. Not yet received. + Sent, + //! When the message is loaded from cache or backfill. + Empty, + //! When the message failed to send + Failed, +}; +Q_ENUM_NS(EventState) } struct DecryptionResult @@ -101,6 +117,7 @@ public: Width, ProportionalHeight, Id, + State, }; QHash roleNames() const override; @@ -137,8 +154,8 @@ private slots: signals: void oldMessagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(const std::string txn_id); - void messageSent(const std::string txn_id, std::string event_id); + void messageFailed(QString txn_id); + void messageSent(QString txn_id, QString event_id); void currentIndexChanged(int index); private: @@ -148,6 +165,7 @@ private: const std::vector &timeline); QHash events; + QSet pending, failed; std::vector eventOrder; QString room_id_; @@ -164,20 +182,37 @@ template void TimelineModel::sendMessage(const T &msg) { - auto txn_id = http::client()->generate_txn_id(); + auto txn_id = http::client()->generate_txn_id(); + mtx::events::RoomEvent msgCopy = {}; + msgCopy.content = msg; + msgCopy.type = mtx::events::EventType::RoomMessage; + msgCopy.event_id = txn_id; + msgCopy.sender = http::client()->user_id().to_string(); + msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + internalAddEvents({msgCopy}); + + QString txn_id_qstr = QString::fromStdString(txn_id); + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size())); + pending.insert(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); + endInsertRows(); + http::client()->send_room_message( room_id_.toStdString(), txn_id, msg, - [this, txn_id](const mtx::responses::EventId &res, mtx::http::RequestErr err) { + [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { if (err) { const int status_code = static_cast(err->status_code); nhlog::net()->warn("[{}] failed to send message: {} {}", txn_id, err->matrix_error.error, status_code); - emit messageFailed(txn_id); + emit messageFailed(txn_id_qstr); } - emit messageSent(txn_id, res.event_id.to_string()); + emit messageSent(txn_id_qstr, QString::fromStdString(res.event_id.to_string())); }); }