From 91d1f19058a31cc35ca1212f042a9dd6f501a7b7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 9 Nov 2019 03:06:10 +0100 Subject: Remove old timeline --- src/timeline/TimelineModel.h | 258 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 src/timeline/TimelineModel.h (limited to 'src/timeline/TimelineModel.h') diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h new file mode 100644 index 00000000..31e41315 --- /dev/null +++ b/src/timeline/TimelineModel.h @@ -0,0 +1,258 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" + +namespace qml_mtx_events { +Q_NAMESPACE + +enum EventType +{ + // Unsupported event + Unsupported, + /// m.room_key_request + KeyRequest, + /// m.room.aliases + Aliases, + /// m.room.avatar + Avatar, + /// m.room.canonical_alias + CanonicalAlias, + /// m.room.create + Create, + /// m.room.encrypted. + Encrypted, + /// m.room.encryption. + Encryption, + /// m.room.guest_access + GuestAccess, + /// m.room.history_visibility + HistoryVisibility, + /// m.room.join_rules + JoinRules, + /// m.room.member + Member, + /// m.room.name + Name, + /// m.room.power_levels + PowerLevels, + /// m.room.tombstone + Tombstone, + /// m.room.topic + Topic, + /// m.room.redaction + Redaction, + /// m.room.pinned_events + PinnedEvents, + // m.sticker + Sticker, + // m.tag + Tag, + /// m.room.message + AudioMessage, + EmoteMessage, + FileMessage, + ImageMessage, + LocationMessage, + NoticeMessage, + TextMessage, + VideoMessage, + Redacted, + 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) +} + +class StateKeeper +{ +public: + StateKeeper(std::function &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function fn_; +}; + +struct DecryptionResult +{ + //! The decrypted content as a normal plaintext event. + mtx::events::collections::TimelineEvents event; + //! Whether or not the decryption was successful. + bool isDecrypted = false; +}; + +class TimelineViewManager; + +class TimelineModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY( + int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + +public: + explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0); + + enum Roles + { + Section, + Type, + Body, + FormattedBody, + UserId, + UserName, + Timestamp, + Url, + ThumbnailUrl, + Filename, + Filesize, + MimeType, + Height, + Width, + ProportionalHeight, + Id, + State, + IsEncrypted, + ReplyTo, + }; + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE QColor userColor(QString id, QColor background); + Q_INVOKABLE QString displayName(QString id) const; + Q_INVOKABLE QString avatarUrl(QString id) const; + Q_INVOKABLE QString formatDateSeparator(QDate date) const; + + Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void openUserProfile(QString userid) const; + Q_INVOKABLE void replyAction(QString id); + Q_INVOKABLE void readReceiptsAction(QString id) const; + Q_INVOKABLE void redactEvent(QString id); + Q_INVOKABLE int idToIndex(QString id) const; + Q_INVOKABLE QString indexToId(int index) const; + + void addEvents(const mtx::responses::Timeline &events); + template + void sendMessage(const T &msg); + +public slots: + void fetchHistory(); + void setCurrentIndex(int index); + int currentIndex() const { return idToIndex(currentId); } + void markEventsAsRead(const std::vector &event_ids); + +private slots: + // Add old events at the top of the timeline. + void addBackwardsEvents(const mtx::responses::Messages &msgs); + +signals: + void oldMessagesRetrieved(const mtx::responses::Messages &res); + void messageFailed(QString txn_id); + void messageSent(QString txn_id, QString event_id); + void currentIndexChanged(int index); + void redactionFailed(QString id); + void eventRedacted(QString id); + +private: + DecryptionResult decryptEvent( + const mtx::events::EncryptedEvent &e) const; + std::vector internalAddEvents( + const std::vector &timeline); + void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content); + void handleClaimedKeys(std::shared_ptr keeper, + const std::map &room_key, + const std::map &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err); + void updateLastMessage(); + void readEvent(const std::string &id); + + QHash events; + QSet pending, failed, read; + std::vector eventOrder; + + QString room_id_; + QString prev_batch_token_; + + bool isInitialSync = true; + bool paginationInProgress = false; + + QHash userColors; + QString currentId; + + TimelineViewManager *manager_; +}; + +template +void +TimelineModel::sendMessage(const T &msg) +{ + 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(); + updateLastMessage(); + + if (cache::client()->isRoomEncrypted(room_id_.toStdString())) + sendEncryptedMessage(txn_id, nlohmann::json(msg)); + else + http::client()->send_room_message( + room_id_.toStdString(), + txn_id, + msg, + [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_qstr); + } + emit messageSent(txn_id_qstr, + QString::fromStdString(res.event_id.to_string())); + }); +} -- cgit 1.5.1 From c424e397b01d8191568f951bdb754e1957681fb8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 10 Nov 2019 00:30:02 +0100 Subject: Add loading spinner and restore message send queue --- resources/qml/TimelineView.qml | 13 +++-- src/timeline/TimelineModel.cpp | 97 +++++++++++++++++++++++++++++++++++- src/timeline/TimelineModel.h | 41 ++++----------- src/timeline/TimelineViewManager.cpp | 3 ++ src/timeline/TimelineViewManager.h | 14 +++--- 5 files changed, 123 insertions(+), 45 deletions(-) (limited to 'src/timeline/TimelineModel.h') diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index b25b3a7c..3bbaa020 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -19,13 +19,20 @@ Item { color: colors.window Text { - visible: !timelineManager.timeline + visible: !timelineManager.timeline && !timelineManager.isInitialSync anchors.centerIn: parent text: qsTr("No room open") font.pointSize: 24 color: colors.windowText } + BusyIndicator { + anchors.centerIn: parent + running: timelineManager.isInitialSync + height: 200 + width: 200 + } + ListView { id: chat @@ -47,10 +54,6 @@ Item { } else { positionViewAtIndex(model.currentIndex, ListView.End) } - - //if (contentHeight < height) { - // model.fetchHistory(); - //} } } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 9cae4608..6b0057a4 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -332,16 +332,18 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj connect( this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { - pending.remove(txn_id); + pending.removeOne(txn_id); failed.insert(txn_id); int idx = idToIndex(txn_id); if (idx < 0) { nhlog::ui()->warn("Failed index out of range"); return; } + isProcessingPending = false; emit dataChanged(index(idx, 0), index(idx, 0)); }); connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + pending.removeOne(txn_id); int idx = idToIndex(txn_id); if (idx < 0) { nhlog::ui()->warn("Sent index out of range"); @@ -365,11 +367,19 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj // ask to be notified for read receipts cache::client()->addPendingReceipt(room_id_, event_id); + isProcessingPending = false; emit dataChanged(index(idx, 0), index(idx, 0)); + + if (pending.size() > 0) + emit nextPendingMessage(); }); connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); }); + + connect( + this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage); + connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage); } QHash @@ -1035,6 +1045,7 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co } catch (const lmdb::error &e) { nhlog::db()->critical( "failed to save megolm outbound session: {}", e.what()); + emit messageFailed(QString::fromStdString(txn_id)); } }); @@ -1044,13 +1055,14 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co http::client()->query_keys( req, - [keeper = std::move(keeper), megolm_payload, this]( + [keeper = std::move(keeper), megolm_payload, txn_id, this]( const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {} {}", err->matrix_error.error, static_cast(err->status_code)); // TODO: Mark the event as failed. Communicate with the UI. + emit messageFailed(QString::fromStdString(txn_id)); return; } @@ -1150,9 +1162,11 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co } catch (const lmdb::error &e) { nhlog::db()->critical( "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical( "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); } } @@ -1241,3 +1255,82 @@ TimelineModel::handleClaimedKeys(std::shared_ptr keeper, (void)keeper; }); } + +struct SendMessageVisitor +{ + SendMessageVisitor(const QString &txn_id, TimelineModel *model) + : txn_id_qstr_(txn_id) + , model_(model) + {} + + template + void operator()(const mtx::events::Event &) + {} + + template::value, int> = 0> + void operator()(const mtx::events::RoomEvent &msg) + + { + if (cache::client()->isRoomEncrypted(model_->room_id_.toStdString())) { + model_->sendEncryptedMessage(txn_id_qstr_.toStdString(), + nlohmann::json(msg.content)); + } else { + QString txn_id_qstr = txn_id_qstr_; + TimelineModel *model = model_; + http::client()->send_room_message( + model->room_id_.toStdString(), + txn_id_qstr.toStdString(), + msg.content, + [txn_id_qstr, model](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_qstr.toStdString(), + err->matrix_error.error, + status_code); + emit model->messageFailed(txn_id_qstr); + } + emit model->messageSent( + txn_id_qstr, QString::fromStdString(res.event_id.to_string())); + }); + } + } + + QString txn_id_qstr_; + TimelineModel *model_; +}; + +void +TimelineModel::processOnePendingMessage() +{ + if (isProcessingPending || pending.isEmpty()) + return; + + isProcessingPending = true; + + QString txn_id_qstr = pending.first(); + + boost::apply_visitor(SendMessageVisitor{txn_id_qstr, this}, events.value(txn_id_qstr)); +} + +void +TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) +{ + internalAddEvents({event}); + + QString txn_id_qstr = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, event); + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size())); + pending.push_back(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); + endInsertRows(); + updateLastMessage(); + + if (!isProcessingPending) + emit nextPendingMessage(); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 31e41315..e7842b99 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -173,6 +173,8 @@ public slots: private slots: // Add old events at the top of the timeline. void addBackwardsEvents(const mtx::responses::Messages &msgs); + void processOnePendingMessage(); + void addPendingMessage(mtx::events::collections::TimelineEvents event); signals: void oldMessagesRetrieved(const mtx::responses::Messages &res); @@ -181,6 +183,8 @@ signals: void currentIndexChanged(int index); void redactionFailed(QString id); void eventRedacted(QString id); + void nextPendingMessage(); + void newMessageToSend(mtx::events::collections::TimelineEvents event); private: DecryptionResult decryptEvent( @@ -198,7 +202,8 @@ private: void readEvent(const std::string &id); QHash events; - QSet pending, failed, read; + QSet failed, read; + QList pending; std::vector eventOrder; QString room_id_; @@ -206,11 +211,14 @@ private: bool isInitialSync = true; bool paginationInProgress = false; + bool isProcessingPending = false; QHash userColors; QString currentId; TimelineViewManager *manager_; + + friend struct SendMessageVisitor; }; template @@ -224,35 +232,6 @@ TimelineModel::sendMessage(const T &msg) 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(); - updateLastMessage(); - if (cache::client()->isRoomEncrypted(room_id_.toStdString())) - sendEncryptedMessage(txn_id, nlohmann::json(msg)); - else - http::client()->send_room_message( - room_id_.toStdString(), - txn_id, - msg, - [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_qstr); - } - emit messageSent(txn_id_qstr, - QString::fromStdString(res.event_id.to_string())); - }); + emit newMessageToSend(msgCopy); } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index d733ad90..06c42a39 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -97,6 +97,9 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms) addRoom(QString::fromStdString(it->first)); models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline); } + + this->isInitialSync_ = false; + emit initialSyncChanged(false); } void diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 691c8ddb..0bc58e68 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -12,10 +12,6 @@ #include "TimelineModel.h" #include "Utils.h" -// temporary for stubs -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - class MxcImageProvider; class ColorImageProvider; @@ -25,6 +21,8 @@ class TimelineViewManager : public QObject Q_PROPERTY( TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) + Q_PROPERTY( + bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) public: TimelineViewManager(QWidget *parent = 0); @@ -36,6 +34,7 @@ public: void clearAll() { models.clear(); } Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } + Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } void openImageOverlay(QString mxcUrl, QString originalFilename, QString mimeType, @@ -66,6 +65,7 @@ signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); + void initialSyncChanged(bool isInitialSync); void mediaCached(QString mxcUrl, QString cacheUrl); public slots: @@ -107,11 +107,11 @@ private: QQuickWidget *view; #endif QWidget *container; - TimelineModel *timeline_ = nullptr; + MxcImageProvider *imgProvider; ColorImageProvider *colorImgProvider; QHash> models; + TimelineModel *timeline_ = nullptr; + bool isInitialSync_ = true; }; - -#pragma GCC diagnostic pop -- cgit 1.5.1 From b8f6e4ce6462f074c34a8b7a286cbabe0e2897aa Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Dec 2019 02:26:41 +0100 Subject: Add encrypted file download --- deps/CMakeLists.txt | 4 +- resources/qml/TimelineRow.qml | 2 +- resources/qml/delegates/FileMessage.qml | 2 +- resources/qml/delegates/ImageMessage.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 4 +- src/timeline/TimelineModel.cpp | 184 +++++++++++++++++++++++ src/timeline/TimelineModel.h | 3 + src/timeline/TimelineViewManager.cpp | 154 ++----------------- src/timeline/TimelineViewManager.h | 27 +--- 9 files changed, 210 insertions(+), 172 deletions(-) (limited to 'src/timeline/TimelineModel.h') diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index d0a715e0..c5932ab7 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -46,10 +46,10 @@ set(BOOST_SHA256 set( MTXCLIENT_URL - https://github.com/Nheko-Reborn/mtxclient/archive/6eee767cc25a9db9f125843e584656cde1ebb6c5.tar.gz + https://github.com/Nheko-Reborn/mtxclient/archive/f719236b08d373d9508f2467bbfc6dfa953b1f8d.zip ) set(MTXCLIENT_HASH - 72fe77da4fed98b3cf069299f66092c820c900359a27ec26070175f9ad208a03) + 0660756c16cf297e02b0b29c07a59fc851723cc65f305893ae7238e6dd2e41c8) set( TWEENY_URL https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 4917e893..2c2ed02a 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -97,7 +97,7 @@ RowLayout { MenuItem { visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage || model.type == MtxEvent.Sticker text: qsTr("Save as") - onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) + onTriggered: timelineManager.timeline.saveMedia(model.id) } } } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index f4cf3f15..2c911c5e 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -31,7 +31,7 @@ Rectangle { } MouseArea { anchors.fill: parent - onClicked: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) + onClicked: timelineManager.timeline.saveMedia(model.id) cursorShape: Qt.PointingHandCursor } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index a1a06012..1b6e5729 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -17,7 +17,7 @@ Item { MouseArea { enabled: model.type == MtxEvent.ImageMessage anchors.fill: parent - onClicked: timelineManager.openImageOverlay(model.url, model.filename, model.mimetype, model.type) + onClicked: timelineManager.openImageOverlay(model.url, model.id) } } } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 3b987545..d0d4d7cb 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -97,7 +97,7 @@ Rectangle { anchors.fill: parent onClicked: { switch (button.state) { - case "": timelineManager.cacheMedia(model.url, model.mimetype); break; + case "": timelineManager.timeline.cacheMedia(model.id); break; case "stopped": media.play(); console.log("play"); button.state = "playing" @@ -118,7 +118,7 @@ Rectangle { } Connections { - target: timelineManager + target: timelineManager.timeline onMediaCached: { if (mxcUrl == model.url) { media.source = "file://" + cacheUrl diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index b904dfd7..f606b603 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -3,11 +3,15 @@ #include #include +#include +#include #include +#include #include "ChatPage.h" #include "Logging.h" #include "MainWindow.h" +#include "MxcImageProvider.h" #include "Olm.h" #include "TimelineViewManager.h" #include "Utils.h" @@ -88,17 +92,42 @@ eventFormattedBody(const mtx::events::RoomEvent &e) } } +template +boost::optional +eventEncryptionInfo(const mtx::events::Event &) +{ + return boost::none; +} + +template +auto +eventEncryptionInfo(const mtx::events::RoomEvent &e) -> std::enable_if_t< + std::is_same>::value, + boost::optional> +{ + return e.content.file; +} + template QString eventUrl(const mtx::events::Event &) { return ""; } + +QString +eventUrl(const mtx::events::StateEvent &e) +{ + return QString::fromStdString(e.content.url); +} + template auto eventUrl(const mtx::events::RoomEvent &e) -> std::enable_if_t::value, QString> { + if (e.content.file) + return QString::fromStdString(e.content.file->url); return QString::fromStdString(e.content.url); } @@ -1342,3 +1371,158 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) if (!isProcessingPending) emit nextPendingMessage(); } + +void +TimelineModel::saveMedia(QString eventId) const +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = + boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event); + QString originalFilename = + boost::apply_visitor([](const auto &e) -> QString { return eventFilename(e); }, event); + QString mimeType = + boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event); + + using EncF = boost::optional; + EncF encryptionInfo = + boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event); + + qml_mtx_events::EventType eventType = boost::apply_visitor( + [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, event); + + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); + } + + QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + + auto filename = QFileDialog::getSaveFileName( + manager_->getWidget(), dialogTitle, originalFilename, filterString); + + if (filename.isEmpty()) + return; + + const auto url = mxcUrl.toStdString(); + + http::client()->download( + url, + [filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), (int)temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); +} + +void +TimelineModel::cacheMedia(QString eventId) +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = + boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event); + QString mimeType = + boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event); + + using EncF = boost::optional; + EncF encryptionInfo = + boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event); + + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString(mxcUrl).remove("mxc://")) + .arg(suffix)); + if (QDir::cleanPath(filename.path()) != filename.path()) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } + + QDir().mkpath(filename.path()); + + if (filename.isReadable()) { + emit mediaCached(mxcUrl, filename.filePath()); + return; + } + + http::client()->download( + url, + [this, mxcUrl, filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + + emit mediaCached(mxcUrl, filename.filePath()); + }); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index e7842b99..f52091e6 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -159,6 +159,8 @@ public: Q_INVOKABLE void redactEvent(QString id); Q_INVOKABLE int idToIndex(QString id) const; Q_INVOKABLE QString indexToId(int index) const; + Q_INVOKABLE void cacheMedia(QString eventId); + Q_INVOKABLE void saveMedia(QString eventId) const; void addEvents(const mtx::responses::Timeline &events); template @@ -185,6 +187,7 @@ signals: void eventRedacted(QString id); void nextPendingMessage(); void newMessageToSend(mtx::events::collections::TimelineEvents event); + void mediaCached(QString mxcUrl, QString cacheUrl); private: DecryptionResult decryptEvent( diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 2a88c882..6430a426 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -1,11 +1,8 @@ #include "TimelineViewManager.h" -#include #include -#include #include #include -#include #include "ChatPage.h" #include "ColorImageProvider.h" @@ -124,146 +121,24 @@ TimelineViewManager::setHistoryView(const QString &room_id) } void -TimelineViewManager::openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const +TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { QQuickImageResponse *imgResponse = imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); - connect(imgResponse, - &QQuickImageResponse::finished, - this, - [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() { - if (!imgResponse->errorString().isEmpty()) { - nhlog::ui()->error("Error when retrieving image for overlay: {}", - imgResponse->errorString().toStdString()); - return; - } - auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - - auto imgDialog = new dialogs::ImageOverlay(pixmap); - imgDialog->show(); - connect(imgDialog, - &dialogs::ImageOverlay::saving, - this, - [this, mxcUrl, originalFilename, mimeType, eventType]() { - saveMedia(mxcUrl, originalFilename, mimeType, eventType); - }); - }); -} - -void -TimelineViewManager::saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const -{ - QString dialogTitle; - if (eventType == qml_mtx_events::EventType::ImageMessage) { - dialogTitle = tr("Save image"); - } else if (eventType == qml_mtx_events::EventType::VideoMessage) { - dialogTitle = tr("Save video"); - } else if (eventType == qml_mtx_events::EventType::AudioMessage) { - dialogTitle = tr("Save audio"); - } else { - dialogTitle = tr("Save file"); - } - - QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); - - auto filename = - QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString); - - if (filename.isEmpty()) - return; - - const auto url = mxcUrl.toStdString(); - - http::client()->download( - url, - [filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), (int)data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - }); -} - -void -TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) -{ - // If the message is a link to a non mxcUrl, don't download it - if (!mxcUrl.startsWith("mxc://")) { - emit mediaCached(mxcUrl, mxcUrl); - return; - } - - QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); - - const auto url = mxcUrl.toStdString(); - QFileInfo filename(QString("%1/media_cache/%2.%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString(mxcUrl).remove("mxc://")) - .arg(suffix)); - if (QDir::cleanPath(filename.path()) != filename.path()) { - nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); - return; - } - - QDir().mkpath(filename.path()); - - if (filename.isReadable()) { - emit mediaCached(mxcUrl, filename.filePath()); - return; - } + connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, imgResponse]() { + if (!imgResponse->errorString().isEmpty()) { + nhlog::ui()->error("Error when retrieving image for overlay: {}", + imgResponse->errorString().toStdString()); + return; + } + auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - http::client()->download( - url, - [this, mxcUrl, filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename.filePath()); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - - emit mediaCached(mxcUrl, filename.filePath()); - }); + auto imgDialog = new dialogs::ImageOverlay(pixmap); + imgDialog->show(); + connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() { + timeline_->saveMedia(eventId); + }); + }); } void @@ -401,3 +276,4 @@ TimelineViewManager::queueVideoMessage(const QString &roomid, video.url = url.toStdString(); models.value(roomid)->sendMessage(video); } + diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 0bc58e68..1cb0de44 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -35,38 +35,13 @@ public: Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } - void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType); - // Qml can only pass enum as int - Q_INVOKABLE void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - openImageOverlay( - mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } - Q_INVOKABLE void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } + Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); void initialSyncChanged(bool isInitialSync); - void mediaCached(QString mxcUrl, QString cacheUrl); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); -- cgit 1.5.1 From 5bfdaff7780bc4299c3edab85c688eebf21f7d4e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Dec 2019 23:34:16 +0100 Subject: Implement decryption of images It is a bit of a hack, but it works... --- CMakeLists.txt | 1 + src/MxcImageProvider.cpp | 9 +++++++-- src/MxcImageProvider.h | 30 ++++++++++++++++++++++++++---- src/timeline/TimelineModel.cpp | 13 +++++++++++++ src/timeline/TimelineModel.h | 2 ++ src/timeline/TimelineViewManager.cpp | 11 ++++++++--- 6 files changed, 57 insertions(+), 9 deletions(-) (limited to 'src/timeline/TimelineModel.h') diff --git a/CMakeLists.txt b/CMakeLists.txt index c918d834..67a1dfb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -365,6 +365,7 @@ qt5_wrap_cpp(MOC_HEADERS src/CommunitiesList.h src/LoginPage.h src/MainWindow.h + src/MxcImageProvider.h src/InviteeItem.h src/QuickSwitcher.h src/RegisterPage.h diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index 556b019b..edf6ceb5 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -5,7 +5,7 @@ void MxcImageResponse::run() { - if (m_requestedSize.isValid()) { + if (m_requestedSize.isValid() && !m_encryptionInfo) { QString fileName = QString("%1_%2x%3_crop") .arg(m_id) .arg(m_requestedSize.width()) @@ -65,7 +65,12 @@ MxcImageResponse::run() return; } - auto data = QByteArray(res.data(), res.size()); + auto temp = res; + if (m_encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, m_encryptionInfo.value())); + + auto data = QByteArray(temp.data(), temp.size()); m_image.loadFromData(data); m_image.setText("original filename", QString::fromStdString(originalFilename)); diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index 19d8a74e..2c197a13 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -6,14 +6,21 @@ #include #include +#include + +#include + class MxcImageResponse : public QQuickImageResponse , public QRunnable { public: - MxcImageResponse(const QString &id, const QSize &requestedSize) + MxcImageResponse(const QString &id, + const QSize &requestedSize, + boost::optional encryptionInfo) : m_id(id) , m_requestedSize(requestedSize) + , m_encryptionInfo(encryptionInfo) { setAutoDelete(false); } @@ -29,19 +36,34 @@ public: QString m_id, m_error; QSize m_requestedSize; QImage m_image; + boost::optional m_encryptionInfo; }; -class MxcImageProvider : public QQuickAsyncImageProvider +class MxcImageProvider + : public QObject + , public QQuickAsyncImageProvider { -public: + Q_OBJECT +public slots: QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override { - MxcImageResponse *response = new MxcImageResponse(id, requestedSize); + boost::optional info; + auto temp = infos.find("mxc://" + id); + if (temp != infos.end()) + info = *temp; + + MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info); pool.start(response); return response; } + void addEncryptionInfo(mtx::crypto::EncryptedFile info) + { + infos.insert(QString::fromStdString(info.url), info); + } + private: QThreadPool pool; + QHash infos; }; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index f606b603..2c58e2f5 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -673,6 +673,19 @@ TimelineModel::internalAddEvents( continue; // don't insert redaction into timeline } + if (auto event = + boost::get>(&e)) { + auto temp = decryptEvent(*event).event; + auto encInfo = boost::apply_visitor( + [](const auto &ev) -> boost::optional { + return eventEncryptionInfo(ev); + }, + temp); + + if (encInfo) + emit newEncryptedImage(encInfo.value()); + } + this->events.insert(id, e); ids.push_back(id); } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index f52091e6..06c64acf 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -6,6 +6,7 @@ #include #include +#include #include #include "Cache.h" @@ -188,6 +189,7 @@ signals: void nextPendingMessage(); void newMessageToSend(mtx::events::collections::TimelineEvents event); void mediaCached(QString mxcUrl, QString cacheUrl); + void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); private: DecryptionResult decryptEvent( diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index c44bcbbf..25f72a6d 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -102,9 +102,14 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms) void TimelineViewManager::addRoom(const QString &room_id) { - if (!models.contains(room_id)) - models.insert(room_id, - QSharedPointer(new TimelineModel(this, room_id))); + if (!models.contains(room_id)) { + QSharedPointer newRoom(new TimelineModel(this, room_id)); + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + imgProvider, + &MxcImageProvider::addEncryptionInfo); + models.insert(room_id, std::move(newRoom)); + } } void -- cgit 1.5.1 From e98a61fea60cf3b95441ce3d9591ced0cf93f566 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 10 Dec 2019 14:46:52 +0100 Subject: Show topic and name changes in timeline --- resources/qml/delegates/MessageDelegate.qml | 12 +++++++++++ resources/qml/delegates/NoticeMessage.qml | 3 ++- src/timeline/TimelineModel.cpp | 32 +++++++++++++++++++++++++++++ src/timeline/TimelineModel.h | 2 ++ 4 files changed, 48 insertions(+), 1 deletion(-) (limited to 'src/timeline/TimelineModel.h') diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 178dfd86..20ec71e5 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -49,6 +49,18 @@ DelegateChooser { text: qsTr("Encryption enabled") } } + DelegateChoice { + roleValue: MtxEvent.Name + NoticeMessage { + notice: model.roomName ? qsTr("room name changed to: %1").arg(model.roomName) : qsTr("removed room name") + } + } + DelegateChoice { + roleValue: MtxEvent.Topic + NoticeMessage { + notice: model.roomTopic ? qsTr("topic changed to: %1").arg(model.roomTopic) : qsTr("removed topic") + } + } DelegateChoice { Placeholder {} } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index a392eb5b..f7467eca 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -1,7 +1,8 @@ import ".." MatrixText { - text: model.formattedBody + property string notice: model.formattedBody.replace("
", "
")
+	text: notice
 	width: parent ? parent.width : undefined
 	font.italic: true
 	color: inactiveColors.text
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index e3d87ae6..9da8a194 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -59,6 +59,30 @@ eventMsgType(const mtx::events::RoomEvent &e) -> decltype(e.content.msgtype)
         return e.content.msgtype;
 }
 
+template
+QString
+eventRoomName(const T &)
+{
+        return "";
+}
+QString
+eventRoomName(const mtx::events::StateEvent &e)
+{
+        return QString::fromStdString(e.content.name);
+}
+
+template
+QString
+eventRoomTopic(const T &)
+{
+        return "";
+}
+QString
+eventRoomTopic(const mtx::events::StateEvent &e)
+{
+        return QString::fromStdString(e.content.topic);
+}
+
 template
 QString
 eventBody(const mtx::events::Event &)
@@ -437,6 +461,8 @@ TimelineModel::roleNames() const
           {State, "state"},
           {IsEncrypted, "isEncrypted"},
           {ReplyTo, "replyTo"},
+          {RoomName, "roomName"},
+          {RoomTopic, "roomTopic"},
         };
 }
 int
@@ -563,6 +589,12 @@ TimelineModel::data(const QModelIndex &index, int role) const
                   [](const auto &e) -> QString { return eventRelatesTo(e); }, event);
                 return QVariant(evId);
         }
+        case RoomName:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventRoomName(e); }, event));
+        case RoomTopic:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventRoomTopic(e); }, event));
         default:
                 return QVariant();
         }
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 06c64acf..05e05962 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -141,6 +141,8 @@ public:
                 State,
                 IsEncrypted,
                 ReplyTo,
+                RoomName,
+                RoomTopic,
         };
 
         QHash roleNames() const override;
-- 
cgit 1.5.1


From 13df852479bf84f297bf59ed99236e52f486a095 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 14 Dec 2019 23:39:02 +0100
Subject: Reduce some include of Cache.h since it needs 11s on average

---
 src/Cache.cpp                 | 101 ++++++++++++++++++-
 src/Cache.h                   | 218 +-----------------------------------------
 src/CacheCryptoStructs.h      |  67 +++++++++++++
 src/CacheStructs.h            |  91 ++++++++++++++++++
 src/ChatPage.h                |   2 +-
 src/CommunitiesList.h         |   3 +-
 src/RoomInfoListItem.cpp      |   2 +-
 src/RoomInfoListItem.h        |   5 +-
 src/dialogs/RoomSettings.cpp  |   8 +-
 src/popups/SuggestionsPopup.h |   2 +
 src/timeline/TimelineModel.h  |   2 +-
 11 files changed, 275 insertions(+), 226 deletions(-)
 create mode 100644 src/CacheCryptoStructs.h
 create mode 100644 src/CacheStructs.h

(limited to 'src/timeline/TimelineModel.h')

diff --git a/src/Cache.cpp b/src/Cache.cpp
index d3aec9db..e61d101e 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -78,6 +78,13 @@ constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions");
 using CachedReceipts = std::multimap>;
 using Receipts       = std::map>;
 
+Q_DECLARE_METATYPE(SearchResult)
+Q_DECLARE_METATYPE(QVector)
+Q_DECLARE_METATYPE(RoomMember)
+Q_DECLARE_METATYPE(mtx::responses::Timeline)
+Q_DECLARE_METATYPE(RoomSearchResult)
+Q_DECLARE_METATYPE(RoomInfo)
+
 namespace {
 std::unique_ptr instance_ = nullptr;
 }
@@ -1504,7 +1511,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
         return "Empty Room";
 }
 
-JoinRule
+mtx::events::state::JoinRule
 Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
         using namespace mtx::events;
@@ -1516,14 +1523,14 @@ Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
 
         if (res) {
                 try {
-                        StateEvent msg =
+                        StateEvent msg =
                           json::parse(std::string(event.data(), event.size()));
                         return msg.content.join_rule;
                 } catch (const json::exception &e) {
                         nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
                 }
         }
-        return JoinRule::Knock;
+        return state::JoinRule::Knock;
 }
 
 bool
@@ -2313,3 +2320,91 @@ from_json(const json &j, RoomInfo &info)
         if (j.count("tags"))
                 info.tags = j.at("tags").get>();
 }
+
+int
+numeric_key_comparison(const MDB_val *a, const MDB_val *b)
+{
+        auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size));
+        auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size));
+
+        if (lhs < rhs)
+                return 1;
+        else if (lhs == rhs)
+                return 0;
+
+        return -1;
+}
+
+void
+to_json(json &j, const ReadReceiptKey &key)
+{
+        j = json{{"event_id", key.event_id}, {"room_id", key.room_id}};
+}
+
+void
+from_json(const json &j, ReadReceiptKey &key)
+{
+        key.event_id = j.at("event_id").get();
+        key.room_id  = j.at("room_id").get();
+}
+
+void
+to_json(json &j, const MemberInfo &info)
+{
+        j["name"]       = info.name;
+        j["avatar_url"] = info.avatar_url;
+}
+
+void
+from_json(const json &j, MemberInfo &info)
+{
+        info.name       = j.at("name");
+        info.avatar_url = j.at("avatar_url");
+}
+
+void
+to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg)
+{
+        obj["session_id"]    = msg.session_id;
+        obj["session_key"]   = msg.session_key;
+        obj["message_index"] = msg.message_index;
+}
+
+void
+from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg)
+{
+        msg.session_id    = obj.at("session_id");
+        msg.session_key   = obj.at("session_key");
+        msg.message_index = obj.at("message_index");
+}
+
+void
+to_json(nlohmann::json &obj, const DevicePublicKeys &msg)
+{
+        obj["ed25519"]    = msg.ed25519;
+        obj["curve25519"] = msg.curve25519;
+}
+
+void
+from_json(const nlohmann::json &obj, DevicePublicKeys &msg)
+{
+        msg.ed25519    = obj.at("ed25519");
+        msg.curve25519 = obj.at("curve25519");
+}
+
+void
+to_json(nlohmann::json &obj, const MegolmSessionIndex &msg)
+{
+        obj["room_id"]    = msg.room_id;
+        obj["session_id"] = msg.session_id;
+        obj["sender_key"] = msg.sender_key;
+}
+
+void
+from_json(const nlohmann::json &obj, MegolmSessionIndex &msg)
+{
+        msg.room_id    = obj.at("room_id");
+        msg.session_id = obj.at("session_id");
+        msg.sender_key = obj.at("sender_key");
+}
+
diff --git a/src/Cache.h b/src/Cache.h
index 878ac9ce..02346287 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -28,224 +28,16 @@
 #include 
 #include 
 
-#include 
 #include 
 #include 
 
+#include "CacheCryptoStructs.h"
+#include "CacheStructs.h"
 #include "Logging.h"
 #include "MatrixClient.h"
 
-using mtx::events::state::JoinRule;
-
-struct RoomMember
-{
-        QString user_id;
-        QString display_name;
-        QImage avatar;
-};
-
-struct SearchResult
-{
-        QString user_id;
-        QString display_name;
-};
-
-static int
-numeric_key_comparison(const MDB_val *a, const MDB_val *b)
-{
-        auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size));
-        auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size));
-
-        if (lhs < rhs)
-                return 1;
-        else if (lhs == rhs)
-                return 0;
-
-        return -1;
-}
-
-Q_DECLARE_METATYPE(SearchResult)
-Q_DECLARE_METATYPE(QVector)
-Q_DECLARE_METATYPE(RoomMember)
-Q_DECLARE_METATYPE(mtx::responses::Timeline)
-
-//! Used to uniquely identify a list of read receipts.
-struct ReadReceiptKey
-{
-        std::string event_id;
-        std::string room_id;
-};
-
-inline void
-to_json(json &j, const ReadReceiptKey &key)
-{
-        j = json{{"event_id", key.event_id}, {"room_id", key.room_id}};
-}
-
-inline void
-from_json(const json &j, ReadReceiptKey &key)
-{
-        key.event_id = j.at("event_id").get();
-        key.room_id  = j.at("room_id").get();
-}
-
-struct DescInfo
-{
-        QString event_id;
-        QString userid;
-        QString body;
-        QString timestamp;
-        QDateTime datetime;
-};
-
-//! UI info associated with a room.
-struct RoomInfo
-{
-        //! The calculated name of the room.
-        std::string name;
-        //! The topic of the room.
-        std::string topic;
-        //! The calculated avatar url of the room.
-        std::string avatar_url;
-        //! The calculated version of this room set at creation time.
-        std::string version;
-        //! Whether or not the room is an invite.
-        bool is_invite = false;
-        //! Total number of members in the room.
-        int16_t member_count = 0;
-        //! Who can access to the room.
-        JoinRule join_rule = JoinRule::Public;
-        bool guest_access  = false;
-        //! Metadata describing the last message in the timeline.
-        DescInfo msgInfo;
-        //! The list of tags associated with this room
-        std::vector tags;
-};
-
-void
-to_json(json &j, const RoomInfo &info);
-
-void
-from_json(const json &j, RoomInfo &info);
-
-//! Basic information per member;
-struct MemberInfo
-{
-        std::string name;
-        std::string avatar_url;
-};
-
-inline void
-to_json(json &j, const MemberInfo &info)
-{
-        j["name"]       = info.name;
-        j["avatar_url"] = info.avatar_url;
-}
-
-inline void
-from_json(const json &j, MemberInfo &info)
-{
-        info.name       = j.at("name");
-        info.avatar_url = j.at("avatar_url");
-}
-
-struct RoomSearchResult
-{
-        std::string room_id;
-        RoomInfo info;
-};
-
-Q_DECLARE_METATYPE(RoomSearchResult)
-Q_DECLARE_METATYPE(RoomInfo)
-
-// Extra information associated with an outbound megolm session.
-struct OutboundGroupSessionData
-{
-        std::string session_id;
-        std::string session_key;
-        uint64_t message_index = 0;
-};
-
-inline void
-to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg)
-{
-        obj["session_id"]    = msg.session_id;
-        obj["session_key"]   = msg.session_key;
-        obj["message_index"] = msg.message_index;
-}
-
-inline void
-from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg)
-{
-        msg.session_id    = obj.at("session_id");
-        msg.session_key   = obj.at("session_key");
-        msg.message_index = obj.at("message_index");
-}
-
-struct OutboundGroupSessionDataRef
-{
-        OlmOutboundGroupSession *session;
-        OutboundGroupSessionData data;
-};
-
-struct DevicePublicKeys
-{
-        std::string ed25519;
-        std::string curve25519;
-};
-
-inline void
-to_json(nlohmann::json &obj, const DevicePublicKeys &msg)
-{
-        obj["ed25519"]    = msg.ed25519;
-        obj["curve25519"] = msg.curve25519;
-}
-
-inline void
-from_json(const nlohmann::json &obj, DevicePublicKeys &msg)
-{
-        msg.ed25519    = obj.at("ed25519");
-        msg.curve25519 = obj.at("curve25519");
-}
-
-//! Represents a unique megolm session identifier.
-struct MegolmSessionIndex
-{
-        //! The room in which this session exists.
-        std::string room_id;
-        //! The session_id of the megolm session.
-        std::string session_id;
-        //! The curve25519 public key of the sender.
-        std::string sender_key;
-};
-
-inline void
-to_json(nlohmann::json &obj, const MegolmSessionIndex &msg)
-{
-        obj["room_id"]    = msg.room_id;
-        obj["session_id"] = msg.session_id;
-        obj["sender_key"] = msg.sender_key;
-}
-
-inline void
-from_json(const nlohmann::json &obj, MegolmSessionIndex &msg)
-{
-        msg.room_id    = obj.at("room_id");
-        msg.session_id = obj.at("session_id");
-        msg.sender_key = obj.at("sender_key");
-}
-
-struct OlmSessionStorage
-{
-        // Megolm sessions
-        std::map group_inbound_sessions;
-        std::map group_outbound_sessions;
-        std::map group_outbound_session_data;
-
-        // Guards for accessing megolm sessions.
-        std::mutex group_outbound_mtx;
-        std::mutex group_inbound_mtx;
-};
+int
+numeric_key_comparison(const MDB_val *a, const MDB_val *b);
 
 class Cache : public QObject
 {
@@ -287,7 +79,7 @@ public:
         //! Calculate & return the name of the room.
         QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
         //! Get room join rules
-        JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb);
+        mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb);
         bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb);
         //! Retrieve the topic of the room if any.
         QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
new file mode 100644
index 00000000..14c9c86b
--- /dev/null
+++ b/src/CacheCryptoStructs.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include 
+#include 
+
+//#include 
+
+#include 
+#include 
+
+// Extra information associated with an outbound megolm session.
+struct OutboundGroupSessionData
+{
+        std::string session_id;
+        std::string session_key;
+        uint64_t message_index = 0;
+};
+
+void
+to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg);
+void
+from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg);
+
+struct OutboundGroupSessionDataRef
+{
+        OlmOutboundGroupSession *session;
+        OutboundGroupSessionData data;
+};
+
+struct DevicePublicKeys
+{
+        std::string ed25519;
+        std::string curve25519;
+};
+
+void
+to_json(nlohmann::json &obj, const DevicePublicKeys &msg);
+void
+from_json(const nlohmann::json &obj, DevicePublicKeys &msg);
+
+//! Represents a unique megolm session identifier.
+struct MegolmSessionIndex
+{
+        //! The room in which this session exists.
+        std::string room_id;
+        //! The session_id of the megolm session.
+        std::string session_id;
+        //! The curve25519 public key of the sender.
+        std::string sender_key;
+};
+
+void
+to_json(nlohmann::json &obj, const MegolmSessionIndex &msg);
+void
+from_json(const nlohmann::json &obj, MegolmSessionIndex &msg);
+
+struct OlmSessionStorage
+{
+        // Megolm sessions
+        std::map group_inbound_sessions;
+        std::map group_outbound_sessions;
+        std::map group_outbound_session_data;
+
+        // Guards for accessing megolm sessions.
+        std::mutex group_outbound_mtx;
+        std::mutex group_inbound_mtx;
+};
diff --git a/src/CacheStructs.h b/src/CacheStructs.h
new file mode 100644
index 00000000..275d20cb
--- /dev/null
+++ b/src/CacheStructs.h
@@ -0,0 +1,91 @@
+#pragma once
+
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+
+struct RoomMember
+{
+        QString user_id;
+        QString display_name;
+        QImage avatar;
+};
+
+struct SearchResult
+{
+        QString user_id;
+        QString display_name;
+};
+
+//! Used to uniquely identify a list of read receipts.
+struct ReadReceiptKey
+{
+        std::string event_id;
+        std::string room_id;
+};
+
+void
+to_json(json &j, const ReadReceiptKey &key);
+
+void
+from_json(const json &j, ReadReceiptKey &key);
+
+struct DescInfo
+{
+        QString event_id;
+        QString userid;
+        QString body;
+        QString timestamp;
+        QDateTime datetime;
+};
+
+//! UI info associated with a room.
+struct RoomInfo
+{
+        //! The calculated name of the room.
+        std::string name;
+        //! The topic of the room.
+        std::string topic;
+        //! The calculated avatar url of the room.
+        std::string avatar_url;
+        //! The calculated version of this room set at creation time.
+        std::string version;
+        //! Whether or not the room is an invite.
+        bool is_invite = false;
+        //! Total number of members in the room.
+        int16_t member_count = 0;
+        //! Who can access to the room.
+        mtx::events::state::JoinRule join_rule = mtx::events::state::JoinRule::Public;
+        bool guest_access                      = false;
+        //! Metadata describing the last message in the timeline.
+        DescInfo msgInfo;
+        //! The list of tags associated with this room
+        std::vector tags;
+};
+
+void
+to_json(json &j, const RoomInfo &info);
+void
+from_json(const json &j, RoomInfo &info);
+
+//! Basic information per member;
+struct MemberInfo
+{
+        std::string name;
+        std::string avatar_url;
+};
+
+void
+to_json(json &j, const MemberInfo &info);
+void
+from_json(const json &j, MemberInfo &info);
+
+struct RoomSearchResult
+{
+        std::string room_id;
+        RoomInfo info;
+};
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 6ca30b3d..6337f800 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -32,7 +32,7 @@
 #include 
 #include 
 
-#include "Cache.h"
+#include "CacheStructs.h"
 #include "CommunitiesList.h"
 #include "MatrixClient.h"
 #include "Utils.h"
diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h
index b18df654..fbb63ff0 100644
--- a/src/CommunitiesList.h
+++ b/src/CommunitiesList.h
@@ -4,7 +4,7 @@
 #include 
 #include 
 
-#include "Cache.h"
+#include "CacheStructs.h"
 #include "CommunitiesListItem.h"
 #include "ui/Theme.h"
 
@@ -53,3 +53,4 @@ private:
 
         std::map> communities_;
 };
+
diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp
index 1e06d914..926e1359 100644
--- a/src/RoomInfoListItem.cpp
+++ b/src/RoomInfoListItem.cpp
@@ -97,7 +97,7 @@ RoomInfoListItem::init(QWidget *parent)
         menu_->addAction(leaveRoom_);
 }
 
-RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent)
+RoomInfoListItem::RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent)
   : QWidget(parent)
   , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined}
   , roomId_(std::move(room_id))
diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h
index 54e02a76..16553c73 100644
--- a/src/RoomInfoListItem.h
+++ b/src/RoomInfoListItem.h
@@ -22,9 +22,10 @@
 #include 
 #include 
 
-#include "Cache.h"
 #include 
 
+#include "CacheStructs.h"
+
 class Menu;
 class RippleOverlay;
 
@@ -64,7 +65,7 @@ class RoomInfoListItem : public QWidget
         Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor)
 
 public:
-        RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent = 0);
+        RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent = 0);
 
         void updateUnreadMessageCount(int count, int highlightedCount);
         void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); };
diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp
index 25909cd8..1be33d33 100644
--- a/src/dialogs/RoomSettings.cpp
+++ b/src/dialogs/RoomSettings.cpp
@@ -248,10 +248,10 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
                         switch (index) {
                         case 0:
                         case 1:
-                                event.join_rule = JoinRule::Public;
+                                event.join_rule = state::JoinRule::Public;
                                 break;
                         default:
-                                event.join_rule = JoinRule::Invite;
+                                event.join_rule = state::JoinRule::Invite;
                         }
 
                         return event;
@@ -260,7 +260,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
                 updateAccessRules(room_id_.toStdString(), join_rule, guest_access);
         });
 
-        if (info_.join_rule == JoinRule::Public) {
+        if (info_.join_rule == state::JoinRule::Public) {
                 if (info_.guest_access) {
                         accessCombo->setCurrentIndex(0);
                 } else {
@@ -342,7 +342,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
         }
 
         // Hide encryption option for public rooms.
-        if (!usesEncryption_ && (info_.join_rule == JoinRule::Public)) {
+        if (!usesEncryption_ && (info_.join_rule == state::JoinRule::Public)) {
                 encryptionToggle_->hide();
                 encryptionLabel->hide();
 
diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h
index 1ef720b2..536c82fb 100644
--- a/src/popups/SuggestionsPopup.h
+++ b/src/popups/SuggestionsPopup.h
@@ -10,6 +10,8 @@
 #include "../ChatPage.h"
 #include "PopupItem.h"
 
+Q_DECLARE_METATYPE(QVector)
+
 class SuggestionsPopup : public QWidget
 {
         Q_OBJECT
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 05e05962..5391c7c1 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -9,7 +9,7 @@
 #include 
 #include 
 
-#include "Cache.h"
+#include "CacheCryptoStructs.h"
 #include "Logging.h"
 #include "MatrixClient.h"
 
-- 
cgit 1.5.1


From 37fbcaf07b205cc0bb89690a415cd06a2814d1a4 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 15 Dec 2019 03:19:33 +0100
Subject: Reduce includes of MatrixClient.h, as it is the most expensive header

---
 src/ChatPage.h                 |  6 +++++-
 src/timeline/TimelineModel.cpp | 10 ++++++++++
 src/timeline/TimelineModel.h   | 12 +++++-------
 3 files changed, 20 insertions(+), 8 deletions(-)

(limited to 'src/timeline/TimelineModel.h')

diff --git a/src/ChatPage.h b/src/ChatPage.h
index 6337f800..a7fb31a8 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -23,6 +23,7 @@
 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -34,7 +35,6 @@
 
 #include "CacheStructs.h"
 #include "CommunitiesList.h"
-#include "MatrixClient.h"
 #include "Utils.h"
 #include "notifications/Manager.h"
 #include "popups/UserMentions.h"
@@ -56,6 +56,10 @@ constexpr int CONSENSUS_TIMEOUT      = 1000;
 constexpr int SHOW_CONTENT_TIMEOUT   = 3000;
 constexpr int TYPING_REFRESH_TIMEOUT = 10000;
 
+namespace mtx::http {
+using RequestErr = const std::optional &;
+}
+
 class ChatPage : public QWidget
 {
         Q_OBJECT
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index d3d1ad34..ce238d94 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -11,6 +11,7 @@
 #include "ChatPage.h"
 #include "Logging.h"
 #include "MainWindow.h"
+#include "MatrixClient.h"
 #include "MxcImageProvider.h"
 #include "Olm.h"
 #include "TimelineViewManager.h"
@@ -1400,6 +1401,15 @@ TimelineModel::processOnePendingMessage()
 void
 TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
 {
+        std::visit(
+          [](auto &msg) {
+                  msg.type             = mtx::events::EventType::RoomMessage;
+                  msg.event_id         = http::client()->generate_txn_id();
+                  msg.sender           = http::client()->user_id().to_string();
+                  msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
+          },
+          event);
+
         internalAddEvents({event});
 
         QString txn_id_qstr =
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 5391c7c1..7ff80c45 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -8,10 +8,14 @@
 
 #include 
 #include 
+#include 
 
 #include "CacheCryptoStructs.h"
 #include "Logging.h"
-#include "MatrixClient.h"
+
+namespace mtx::http {
+using RequestErr = const std::optional &;
+}
 
 namespace qml_mtx_events {
 Q_NAMESPACE
@@ -232,13 +236,7 @@ template
 void
 TimelineModel::sendMessage(const T &msg)
 {
-        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();
-
         emit newMessageToSend(msgCopy);
 }
-- 
cgit 1.5.1


From 946ab4d0f287307c24e310c6d2faef931f094ec5 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 3 Jan 2020 23:21:33 +0100
Subject: invert timeline

---
 resources/qml/TimelineView.qml | 56 +++++++++++++++++++++++++++++++++++-------
 src/timeline/TimelineModel.cpp | 52 ++++++++++++++++++++++++++++++---------
 src/timeline/TimelineModel.h   |  1 +
 3 files changed, 88 insertions(+), 21 deletions(-)

(limited to 'src/timeline/TimelineModel.h')

diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 1a1900ad..6bc2eb53 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -115,30 +115,68 @@ Item {
 			onMovementEnded: updatePosition()
 
 			spacing: 4
-			delegate: TimelineRow {
+			verticalLayoutDirection: ListView.BottomToTop
+
+			delegate: Rectangle {
+				// This would normally be previousSection, but our model's order is inverted.
+				property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
+
+				id: wrapper
+				property Item section
+				width: chat.width
+				height: section ? section.height + timelinerow.height : timelinerow.height
+
+				TimelineRow {
+					id: timelinerow
+					y: section ? section.y + section.height : 0
+				}
 				function isFullyVisible() {
 					return height > 1 && (y - chat.contentY - 1) + height < chat.height
 				}
 				function getIndex() {
 					return index;
 				}
+
+				onSectionBoundaryChanged: {
+					if (sectionBoundary) {
+						var properties = {
+							'modelData': model.dump,
+							'section': ListView.section,
+							'nextSection': ListView.nextSection
+						}
+						section = sectionHeader.createObject(wrapper, properties)
+					} else {
+						section.destroy()
+						section = null
+					}
+				}
+
 			}
 
 			section {
 				property: "section"
-				delegate: Column {
+			}
+			Component {
+				id: sectionHeader
+				Column {
+					property var modelData
+					property string section
+					property string nextSection
+
 					topPadding: 4
 					bottomPadding: 4
 					spacing: 8
 
+					visible: !!modelData
+
 					width: parent.width
 					height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
 
 					Label {
 						id: dateBubble
-						anchors.horizontalCenter: parent.horizontalCenter
+						anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
 						visible: section.includes(" ")
-						text: chat.model.formatDateSeparator(new Date(Number(section.split(" ")[1])))
+						text: chat.model.formatDateSeparator(modelData.timestamp)
 						color: colors.windowText
 
 						height: contentHeight * 1.2
@@ -155,20 +193,20 @@ Item {
 						Avatar {
 							width: avatarSize
 							height: avatarSize
-							url: chat.model.avatarUrl(section.split(" ")[0]).replace("mxc://", "image://MxcImage/")
-							displayName: chat.model.displayName(section.split(" ")[0])
+							url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
+							displayName: modelData.userName
 
 							MouseArea {
 								anchors.fill: parent
-								onClicked: chat.model.openUserProfile(section.split(" ")[0])
+								onClicked: chat.model.openUserProfile(modelData.userId)
 								cursorShape: Qt.PointingHandCursor
 							}
 						}
 
 						Text { 
 							id: userName
-							text: chat.model.escapeEmoji(chat.model.displayName(section.split(" ")[0]))
-							color: chat.model.userColor(section.split(" ")[0], colors.window)
+							text: chat.model.escapeEmoji(modelData.userName)
+							color: chat.model.userColor(modelData.userId, colors.window)
 							textFormat: Text.RichText
 
 							MouseArea {
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 593a21df..8746a31f 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -202,6 +202,7 @@ TimelineModel::roleNames() const
           {ReplyTo, "replyTo"},
           {RoomName, "roomName"},
           {RoomTopic, "roomTopic"},
+          {Dump, "dump"},
         };
 }
 int
@@ -235,7 +236,7 @@ TimelineModel::data(const QModelIndex &index, int role) const
 
                 std::string userId = acc::sender(event);
 
-                for (int r = index.row() - 1; r > 0; r--) {
+                for (size_t r = index.row() + 1; r < eventOrder.size(); r++) {
                         auto tempEv        = events.value(eventOrder[r]);
                         QDateTime prevDate = origin_server_ts(tempEv);
                         prevDate.setTime(QTime());
@@ -314,6 +315,35 @@ TimelineModel::data(const QModelIndex &index, int role) const
                 return QVariant(QString::fromStdString(room_name(event)));
         case RoomTopic:
                 return QVariant(QString::fromStdString(room_topic(event)));
+        case Dump: {
+                QVariantMap m;
+                auto names = roleNames();
+
+                // m.insert(names[Section], data(index, static_cast(Section)));
+                m.insert(names[Type], data(index, static_cast(Type)));
+                m.insert(names[Body], data(index, static_cast(Body)));
+                m.insert(names[FormattedBody], data(index, static_cast(FormattedBody)));
+                m.insert(names[UserId], data(index, static_cast(UserId)));
+                m.insert(names[UserName], data(index, static_cast(UserName)));
+                m.insert(names[Timestamp], data(index, static_cast(Timestamp)));
+                m.insert(names[Url], data(index, static_cast(Url)));
+                m.insert(names[ThumbnailUrl], data(index, static_cast(ThumbnailUrl)));
+                m.insert(names[Filename], data(index, static_cast(Filename)));
+                m.insert(names[Filesize], data(index, static_cast(Filesize)));
+                m.insert(names[MimeType], data(index, static_cast(MimeType)));
+                m.insert(names[Height], data(index, static_cast(Height)));
+                m.insert(names[Width], data(index, static_cast(Width)));
+                m.insert(names[ProportionalHeight],
+                         data(index, static_cast(ProportionalHeight)));
+                m.insert(names[Id], data(index, static_cast(Id)));
+                m.insert(names[State], data(index, static_cast(State)));
+                m.insert(names[IsEncrypted], data(index, static_cast(IsEncrypted)));
+                m.insert(names[ReplyTo], data(index, static_cast(ReplyTo)));
+                m.insert(names[RoomName], data(index, static_cast(RoomName)));
+                m.insert(names[RoomTopic], data(index, static_cast(RoomTopic)));
+
+                return QVariant(m);
+        }
         default:
                 return QVariant();
         }
@@ -335,10 +365,8 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
         if (ids.empty())
                 return;
 
-        beginInsertRows(QModelIndex(),
-                        static_cast(this->eventOrder.size()),
-                        static_cast(this->eventOrder.size() + ids.size() - 1));
-        this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end());
+        beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1));
+        this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend());
         endInsertRows();
 
         updateLastMessage();
@@ -362,7 +390,7 @@ isMessage(const mtx::events::Event &)
 void
 TimelineModel::updateLastMessage()
 {
-        for (auto it = eventOrder.rbegin(); it != eventOrder.rend(); ++it) {
+        for (auto it = eventOrder.begin(); it != eventOrder.end(); ++it) {
                 auto event = events.value(*it);
                 if (auto e = std::get_if>(
                       &event)) {
@@ -499,8 +527,10 @@ TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs)
         std::vector ids = internalAddEvents(msgs.chunk);
 
         if (!ids.empty()) {
-                beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1));
-                this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend());
+                beginInsertRows(QModelIndex(),
+                                static_cast(this->eventOrder.size()),
+                                static_cast(this->eventOrder.size() + ids.size() - 1));
+                this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end());
                 endInsertRows();
         }
 
@@ -1120,11 +1150,9 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
         internalAddEvents({event});
 
         QString txn_id_qstr = QString::fromStdString(mtx::accessors::event_id(event));
-        beginInsertRows(QModelIndex(),
-                        static_cast(this->eventOrder.size()),
-                        static_cast(this->eventOrder.size()));
+        beginInsertRows(QModelIndex(), 0, 0);
         pending.push_back(txn_id_qstr);
-        this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr);
+        this->eventOrder.insert(this->eventOrder.begin(), txn_id_qstr);
         endInsertRows();
         updateLastMessage();
 
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 7ff80c45..4161a0fc 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -147,6 +147,7 @@ public:
                 ReplyTo,
                 RoomName,
                 RoomTopic,
+                Dump,
         };
 
         QHash roleNames() const override;
-- 
cgit 1.5.1


From 5af6f6528ba05974edd359833194c6f1dd3e1c96 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Mon, 6 Jan 2020 16:42:56 +0100
Subject: Use fetchMore for native lazy loading of item model data

---
 resources/qml/TimelineView.qml |  6 +---
 src/timeline/TimelineModel.cpp | 75 +++++++++++++++++++++++++-----------------
 src/timeline/TimelineModel.h   |  4 ++-
 3 files changed, 48 insertions(+), 37 deletions(-)

(limited to 'src/timeline/TimelineModel.h')

diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 6bc2eb53..18ae3793 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -93,13 +93,9 @@ Item {
 					currentIndex = newIndex
 					model.currentIndex = newIndex
 				}
-
-				if (contentHeight < height && model) {
-					model.fetchHistory();
-				}
 			}
 
-			onAtYBeginningChanged: if (atYBeginning) { chat.model.currentIndex = 0; chat.currentIndex = 0; model.fetchHistory(); }
+			onAtYBeginningChanged: if (atYBeginning) { chat.model.currentIndex = 0; chat.currentIndex = 0; }
 
 			function updatePosition() {
 				for (var y = chat.contentY + chat.height; y > chat.height; y -= 9) {
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 8746a31f..3dafb8c2 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -349,6 +349,50 @@ TimelineModel::data(const QModelIndex &index, int role) const
         }
 }
 
+bool
+TimelineModel::canFetchMore(const QModelIndex &) const
+{
+        if (eventOrder.empty())
+                return true;
+        if (!std::holds_alternative>(
+              events[eventOrder.back()]))
+                return true;
+        else
+
+                return false;
+}
+
+void
+TimelineModel::fetchMore(const QModelIndex &)
+{
+        if (paginationInProgress) {
+                nhlog::ui()->warn("Already loading older messages");
+                return;
+        }
+
+        paginationInProgress = true;
+        mtx::http::MessagesOpts opts;
+        opts.room_id = room_id_.toStdString();
+        opts.from    = prev_batch_token_.toStdString();
+
+        nhlog::ui()->debug("Paginationg room {}", opts.room_id);
+
+        http::client()->messages(
+          opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->error("failed to call /messages ({}): {} - {}",
+                                              opts.room_id,
+                                              mtx::errors::to_string(err->matrix_error.errcode),
+                                              err->matrix_error.error);
+                          paginationInProgress = false;
+                          return;
+                  }
+
+                  emit oldMessagesRetrieved(std::move(res));
+                  paginationInProgress = false;
+          });
+}
+
 void
 TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
 {
@@ -465,37 +509,6 @@ TimelineModel::internalAddEvents(
         return ids;
 }
 
-void
-TimelineModel::fetchHistory()
-{
-        if (paginationInProgress) {
-                nhlog::ui()->warn("Already loading older messages");
-                return;
-        }
-
-        paginationInProgress = true;
-        mtx::http::MessagesOpts opts;
-        opts.room_id = room_id_.toStdString();
-        opts.from    = prev_batch_token_.toStdString();
-
-        nhlog::ui()->info("Paginationg room {}", opts.room_id);
-
-        http::client()->messages(
-          opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->error("failed to call /messages ({}): {} - {}",
-                                              opts.room_id,
-                                              mtx::errors::to_string(err->matrix_error.errcode),
-                                              err->matrix_error.error);
-                          paginationInProgress = false;
-                          return;
-                  }
-
-                  emit oldMessagesRetrieved(std::move(res));
-                  paginationInProgress = false;
-          });
-}
-
 void
 TimelineModel::setCurrentIndex(int index)
 {
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 4161a0fc..0f18f7ef 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -154,6 +154,9 @@ public:
         int rowCount(const QModelIndex &parent = QModelIndex()) const override;
         QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
 
+        bool canFetchMore(const QModelIndex &) const override;
+        void fetchMore(const QModelIndex &) override;
+
         Q_INVOKABLE QColor userColor(QString id, QColor background);
         Q_INVOKABLE QString displayName(QString id) const;
         Q_INVOKABLE QString avatarUrl(QString id) const;
@@ -175,7 +178,6 @@ public:
         void sendMessage(const T &msg);
 
 public slots:
-        void fetchHistory();
         void setCurrentIndex(int index);
         int currentIndex() const { return idToIndex(currentId); }
         void markEventsAsRead(const std::vector &event_ids);
-- 
cgit 1.5.1


From 2b3dc3d8b9d1108c3f6ad226ad65060bd3999033 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 11 Jan 2020 14:07:51 +0100
Subject: Implement fancy reply rendering

This currently assumes the event, that is replied to, is already
fetched. If it isn't, it will render an empty reply. In the future we
should fetch replies before rendering them.
---
 resources/qml/TimelineRow.qml                    |  61 +++++++++--
 resources/qml/delegates/FileMessage.qml          |   6 +-
 resources/qml/delegates/ImageMessage.qml         |  12 +--
 resources/qml/delegates/MessageDelegate.qml      | 128 +++++++++++++----------
 resources/qml/delegates/NoticeMessage.qml        |   2 +-
 resources/qml/delegates/Placeholder.qml          |   2 +-
 resources/qml/delegates/PlayableMediaMessage.qml |  16 +--
 resources/qml/delegates/TextMessage.qml          |   2 +-
 src/timeline/TimelineModel.cpp                   |  16 ++-
 src/timeline/TimelineModel.h                     |   1 +
 10 files changed, 159 insertions(+), 87 deletions(-)

(limited to 'src/timeline/TimelineModel.h')

diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 2c2ed02a..86780413 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -14,23 +14,70 @@ RowLayout {
 	anchors.left: parent.left
 	anchors.right: parent.right
 
-	height: Math.max(contentItem.height, 16)
+	//height: Math.max(model.replyTo ? reply.height + contentItem.height + 4 : contentItem.height, 16)
 
 	Column {
 		Layout.fillWidth: true
 		Layout.alignment: Qt.AlignTop
+		spacing: 4
 
-		//property var replyTo: model.replyTo
+		// fancy reply, if this is a reply
+		Rectangle {
+			visible: model.replyTo
+			width: parent.width
+			height: replyContainer.height
+
+			Rectangle {
+				id: colorLine
+				height: replyContainer.height
+				width: 4
+				color: chat.model.userColor(reply.modelData.userId, colors.window)
+			}
+
+			Column {
+				id: replyContainer
+				anchors.left: colorLine.right
+				anchors.leftMargin: 4
+				width: parent.width - 8
+
+
+				Text { 
+					id: userName
+					text: chat.model.escapeEmoji(reply.modelData.userName)
+					color: chat.model.userColor(reply.modelData.userId, colors.window)
+					textFormat: Text.RichText
+
+					MouseArea {
+						anchors.fill: parent
+						onClicked: chat.model.openUserProfile(reply.modelData.userId)
+						cursorShape: Qt.PointingHandCursor
+					}
+				}
+
+				MessageDelegate {
+					id: reply
+					width: parent.width
 
-		//Text {
-		//	property int idx: timelineManager.timeline.idToIndex(replyTo)
-		//	text: "" + (idx != -1 ? timelineManager.timeline.data(timelineManager.timeline.index(idx, 0), 2) : "nothing")
-		//}
+					modelData: chat.model.getDump(model.replyTo)
+				}
+			}
+
+			color: { var col = chat.model.userColor(reply.modelData.userId, colors.window); col.a = 0.2; return col }
+
+			MouseArea {
+				anchors.fill: parent
+				onClicked: chat.positionViewAtIndex(chat.model.idToIndex(model.replyTo), ListView.Contain)
+				cursorShape: Qt.PointingHandCursor
+			}
+		}
+
+		// actual message content
 		MessageDelegate {
 			id: contentItem
 
 			width: parent.width
-			height: childrenRect.height
+
+			modelData: model
 		}
 	}
 
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index 2c911c5e..9a5300bb 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -31,7 +31,7 @@ Rectangle {
 			}
 			MouseArea {
 				anchors.fill: parent
-				onClicked: timelineManager.timeline.saveMedia(model.id)
+				onClicked: timelineManager.timeline.saveMedia(model.data.id)
 				cursorShape: Qt.PointingHandCursor
 			}
 		}
@@ -40,14 +40,14 @@ Rectangle {
 
 			Text {
 				Layout.fillWidth: true
-				text: model.body
+				text: model.data.body
 				textFormat: Text.PlainText
 				elide: Text.ElideRight
 				color: colors.text
 			}
 			Text {
 				Layout.fillWidth: true
-				text: model.filesize
+				text: model.data.filesize
 				textFormat: Text.PlainText
 				elide: Text.ElideRight
 				color: colors.text
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index 15ce29b7..3393f043 100644
--- a/resources/qml/delegates/ImageMessage.qml
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -3,26 +3,26 @@ import QtQuick 2.6
 import im.nheko 1.0
 
 Item {
-	property double tempWidth: Math.min(parent ? parent.width : undefined, model.width)
-	property double tempHeight: tempWidth * model.proportionalHeight
+	property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width)
+	property double tempHeight: tempWidth * model.data.proportionalHeight
 
 	property bool tooHigh: tempHeight > chat.height - 40
 
 	height: tooHigh ? chat.height - 40 : tempHeight
-	width: tooHigh ? (chat.height - 40) / model.proportionalHeight : tempWidth
+	width: tooHigh ? (chat.height - 40) / model.data.proportionalHeight : tempWidth
 
 	Image {
 		id: img
 		anchors.fill: parent
 
-		source: model.url.replace("mxc://", "image://MxcImage/")
+		source: model.data.url.replace("mxc://", "image://MxcImage/")
 		asynchronous: true
 		fillMode: Image.PreserveAspectFit
 
 		MouseArea {
-			enabled: model.type == MtxEvent.ImageMessage
+			enabled: model.data.type == MtxEvent.ImageMessage
 			anchors.fill: parent
-			onClicked: timelineManager.openImageOverlay(model.url, model.id)
+			onClicked: timelineManager.openImageOverlay(model.data.url, model.data.id)
 		}
 	}
 }
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 20ec71e5..1716d2d4 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -1,67 +1,81 @@
 import QtQuick 2.6
 import im.nheko 1.0
 
-DelegateChooser {
-	//role: "type" //< not supported in our custom implementation, have to use roleValue
-	roleValue: model.type
-
-	DelegateChoice {
-		roleValue: MtxEvent.TextMessage
-		TextMessage {}
-	}
-	DelegateChoice {
-		roleValue: MtxEvent.NoticeMessage
-		NoticeMessage {}
-	}
-	DelegateChoice {
-		roleValue: MtxEvent.EmoteMessage
-		TextMessage {}
-	}
-	DelegateChoice {
-		roleValue: MtxEvent.ImageMessage
-		ImageMessage {}
-	}
-	DelegateChoice {
-		roleValue: MtxEvent.Sticker
-		ImageMessage {}
-	}
-	DelegateChoice {
-		roleValue: MtxEvent.FileMessage
-		FileMessage {}
+Item {
+	// Workaround to have an assignable global property
+	Item {
+		id: model
+		property var data;
 	}
-	DelegateChoice {
-		roleValue: MtxEvent.VideoMessage
-		PlayableMediaMessage {}
-	}
-	DelegateChoice {
-		roleValue: MtxEvent.AudioMessage
-		PlayableMediaMessage {}
-	}
-	DelegateChoice {
-		roleValue: MtxEvent.Redacted
-		Pill {
-			text: qsTr("redacted")
+	
+	property alias modelData: model.data
+
+	height: chooser.childrenRect.height
+
+	DelegateChooser {
+		id: chooser
+		//role: "type" //< not supported in our custom implementation, have to use roleValue
+		roleValue: model.data.type
+		anchors.fill: parent
+
+		DelegateChoice {
+			roleValue: MtxEvent.TextMessage
+			TextMessage {}
 		}
-	}
-	DelegateChoice {
-		roleValue: MtxEvent.Encryption
-		Pill {
-			text: qsTr("Encryption enabled")
+		DelegateChoice {
+			roleValue: MtxEvent.NoticeMessage
+			NoticeMessage {}
 		}
-	}
-	DelegateChoice {
-		roleValue: MtxEvent.Name
-		NoticeMessage {
-			notice: model.roomName ? qsTr("room name changed to: %1").arg(model.roomName) : qsTr("removed room name")
+		DelegateChoice {
+			roleValue: MtxEvent.EmoteMessage
+			TextMessage {}
 		}
-	}
-	DelegateChoice {
-		roleValue: MtxEvent.Topic
-		NoticeMessage {
-			notice: model.roomTopic ? qsTr("topic changed to: %1").arg(model.roomTopic) : qsTr("removed topic")
+		DelegateChoice {
+			roleValue: MtxEvent.ImageMessage
+			ImageMessage {}
+		}
+		DelegateChoice {
+			roleValue: MtxEvent.Sticker
+			ImageMessage {}
+		}
+		DelegateChoice {
+			roleValue: MtxEvent.FileMessage
+			FileMessage {}
+		}
+		DelegateChoice {
+			roleValue: MtxEvent.VideoMessage
+			PlayableMediaMessage {}
+		}
+		DelegateChoice {
+			roleValue: MtxEvent.AudioMessage
+			PlayableMediaMessage {}
+		}
+		DelegateChoice {
+			roleValue: MtxEvent.Redacted
+			Pill {
+				text: qsTr("redacted")
+			}
+		}
+		DelegateChoice {
+			roleValue: MtxEvent.Encryption
+			Pill {
+				text: qsTr("Encryption enabled")
+			}
+		}
+		DelegateChoice {
+			roleValue: MtxEvent.Name
+			NoticeMessage {
+				notice: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name")
+			}
+		}
+		DelegateChoice {
+			roleValue: MtxEvent.Topic
+			NoticeMessage {
+				notice: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic")
+			}
+		}
+		DelegateChoice {
+			Placeholder {}
 		}
-	}
-	DelegateChoice {
-		Placeholder {}
 	}
 }
diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml
index f7467eca..34132bcf 100644
--- a/resources/qml/delegates/NoticeMessage.qml
+++ b/resources/qml/delegates/NoticeMessage.qml
@@ -1,7 +1,7 @@
 import ".."
 
 MatrixText {
-	property string notice: model.formattedBody.replace("
", "
")
+	property string notice: model.data.formattedBody.replace("
", "
")
 	text: notice
 	width: parent ? parent.width : undefined
 	font.italic: true
diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml
index 4c0e68c3..36d7b2bc 100644
--- a/resources/qml/delegates/Placeholder.qml
+++ b/resources/qml/delegates/Placeholder.qml
@@ -1,7 +1,7 @@
 import ".."
 
 MatrixText {
-	text: qsTr("unimplemented event: ") + model.type
+	text: qsTr("unimplemented event: ") + model.data.type
 	width: parent ? parent.width : undefined
 	color: inactiveColors.text
 }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index b3275462..ebf7487c 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -19,12 +19,12 @@ Rectangle {
 
 		Rectangle {
 			id: videoContainer
-			visible: model.type == MtxEvent.VideoMessage
-			width: Math.min(parent.width, model.width ? model.width : 400) // some media has 0 as size...
-			height: width*model.proportionalHeight
+			visible: model.data.type == MtxEvent.VideoMessage
+			width: Math.min(parent.width, model.data.width ? model.data.width : 400) // some media has 0 as size...
+			height: width*model.data.proportionalHeight
 			Image {
 				anchors.fill: parent
-				source: model.thumbnailUrl.replace("mxc://", "image://MxcImage/")
+				source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/")
 				asynchronous: true
 				fillMode: Image.PreserveAspectFit
 
@@ -97,7 +97,7 @@ Rectangle {
 					anchors.fill: parent
 					onClicked: {
 						switch (button.state) {
-							case "": timelineManager.timeline.cacheMedia(model.id); break;
+							case "": timelineManager.timeline.cacheMedia(model.data.id); break;
 							case "stopped":
 							media.play(); console.log("play");
 							button.state = "playing"
@@ -120,7 +120,7 @@ Rectangle {
 				Connections {
 					target: timelineManager.timeline
 					onMediaCached: {
-						if (mxcUrl == model.url) {
+						if (mxcUrl == model.data.url) {
 							media.source = "file://" + cacheUrl
 							button.state = "stopped"
 							console.log("media loaded: " + mxcUrl + " at " + cacheUrl)
@@ -145,14 +145,14 @@ Rectangle {
 
 				Text {
 					Layout.fillWidth: true
-					text: model.body
+					text: model.data.body
 					textFormat: Text.PlainText
 					elide: Text.ElideRight
 					color: colors.text
 				}
 				Text {
 					Layout.fillWidth: true
-					text: model.filesize
+					text: model.data.filesize
 					textFormat: Text.PlainText
 					elide: Text.ElideRight
 					color: colors.text
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index f984b32f..92ba560b 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -1,6 +1,6 @@
 import ".."
 
 MatrixText {
-	text: model.formattedBody.replace("
", "
")
+	text: model.data.formattedBody.replace("
", "
")
 	width: parent ? parent.width : undefined
 }
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 0e7f5259..41d864bd 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -212,6 +212,14 @@ TimelineModel::rowCount(const QModelIndex &parent) const
         return (int)this->eventOrder.size();
 }
 
+QVariantMap
+TimelineModel::getDump(QString eventId) const
+{
+        if (events.contains(eventId))
+                return data(index(idToIndex(eventId), 0), Dump).toMap();
+        return {};
+}
+
 QVariant
 TimelineModel::data(const QModelIndex &index, int role) const
 {
@@ -263,11 +271,13 @@ TimelineModel::data(const QModelIndex &index, int role) const
                 return QVariant(toRoomEventType(event));
         case Body:
                 return QVariant(utils::replaceEmoji(QString::fromStdString(body(event))));
-        case FormattedBody:
+        case FormattedBody: {
+                const static QRegularExpression replyFallback(
+                  ".*", QRegularExpression::DotMatchesEverythingOption);
                 return QVariant(
                   utils::replaceEmoji(utils::linkifyMessage(formattedBodyWithFallback(event)))
-                    .remove("")
-                    .remove(""));
+                    .remove(replyFallback));
+        }
         case Url:
                 return QVariant(QString::fromStdString(url(event)));
         case ThumbnailUrl:
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 0f18f7ef..61dd6b69 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -181,6 +181,7 @@ public slots:
         void setCurrentIndex(int index);
         int currentIndex() const { return idToIndex(currentId); }
         void markEventsAsRead(const std::vector &event_ids);
+        QVariantMap getDump(QString eventId) const;
 
 private slots:
         // Add old events at the top of the timeline.
-- 
cgit 1.5.1


From 4727f1c2bbeb5ba10299e716f681bd22bd92f16c Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 11 Jan 2020 18:53:32 +0100
Subject: Fetch missing events for replies

---
 resources/qml/delegates/MessageDelegate.qml |   4 +
 src/timeline/TimelineModel.cpp              | 149 ++++++++++++++++++----------
 src/timeline/TimelineModel.h                |   2 +
 src/timeline/TimelineViewManager.cpp        |   3 +
 4 files changed, 105 insertions(+), 53 deletions(-)

(limited to 'src/timeline/TimelineModel.h')

diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 1716d2d4..ae18d505 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -18,6 +18,10 @@ Item {
 		roleValue: model.data.type
 		anchors.fill: parent
 
+		DelegateChoice {
+			roleValue: MtxEvent.UnknownMessage
+			Placeholder { text: "Unretrieved event" }
+		}
 		DelegateChoice {
 			roleValue: MtxEvent.TextMessage
 			TextMessage {}
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 41d864bd..a1367bf3 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -70,8 +70,9 @@ struct RoomEventType
                 case EventType::Tag:
                         return qml_mtx_events::EventType::Tag;
                 case EventType::Unsupported:
-                default:
                         return qml_mtx_events::EventType::Unsupported;
+                default:
+                        return qml_mtx_events::EventType::UnknownMessage;
                 }
         }
         qml_mtx_events::EventType operator()(const mtx::events::Event &)
@@ -175,6 +176,17 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
         connect(
           this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage);
         connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage);
+
+        connect(this,
+                &TimelineModel::replyFetched,
+                this,
+                [this](QString requestingEvent, mtx::events::collections::TimelineEvents event) {
+                        events.insert(QString::fromStdString(mtx::accessors::event_id(event)),
+                                      event);
+                        auto idx = idToIndex(requestingEvent);
+                        if (idx >= 0)
+                                emit dataChanged(index(idx, 0), index(idx, 0));
+                });
 }
 
 QHash
@@ -216,20 +228,15 @@ QVariantMap
 TimelineModel::getDump(QString eventId) const
 {
         if (events.contains(eventId))
-                return data(index(idToIndex(eventId), 0), Dump).toMap();
+                return data(eventId, Dump).toMap();
         return {};
 }
 
 QVariant
-TimelineModel::data(const QModelIndex &index, int role) const
+TimelineModel::data(const QString &id, int role) const
 {
         using namespace mtx::accessors;
-        namespace acc = mtx::accessors;
-        if (index.row() < 0 && index.row() >= (int)eventOrder.size())
-                return QVariant();
-
-        QString id = eventOrder[index.row()];
-
+        namespace acc                                  = mtx::accessors;
         mtx::events::collections::TimelineEvents event = events.value(id);
 
         if (auto e =
@@ -238,28 +245,6 @@ TimelineModel::data(const QModelIndex &index, int role) const
         }
 
         switch (role) {
-        case Section: {
-                QDateTime date = origin_server_ts(event);
-                date.setTime(QTime());
-
-                std::string userId = acc::sender(event);
-
-                for (size_t r = index.row() + 1; r < eventOrder.size(); r++) {
-                        auto tempEv        = events.value(eventOrder[r]);
-                        QDateTime prevDate = origin_server_ts(tempEv);
-                        prevDate.setTime(QTime());
-                        if (prevDate != date)
-                                return QString("%2 %1")
-                                  .arg(date.toMSecsSinceEpoch())
-                                  .arg(QString::fromStdString(userId));
-
-                        std::string prevUserId = acc::sender(tempEv);
-                        if (userId != prevUserId)
-                                break;
-                }
-
-                return QString("%1").arg(QString::fromStdString(userId));
-        }
         case UserId:
                 return QVariant(QString::fromStdString(acc::sender(event)));
         case UserName:
@@ -329,28 +314,27 @@ TimelineModel::data(const QModelIndex &index, int role) const
                 QVariantMap m;
                 auto names = roleNames();
 
-                // m.insert(names[Section], data(index, static_cast(Section)));
-                m.insert(names[Type], data(index, static_cast(Type)));
-                m.insert(names[Body], data(index, static_cast(Body)));
-                m.insert(names[FormattedBody], data(index, static_cast(FormattedBody)));
-                m.insert(names[UserId], data(index, static_cast(UserId)));
-                m.insert(names[UserName], data(index, static_cast(UserName)));
-                m.insert(names[Timestamp], data(index, static_cast(Timestamp)));
-                m.insert(names[Url], data(index, static_cast(Url)));
-                m.insert(names[ThumbnailUrl], data(index, static_cast(ThumbnailUrl)));
-                m.insert(names[Filename], data(index, static_cast(Filename)));
-                m.insert(names[Filesize], data(index, static_cast(Filesize)));
-                m.insert(names[MimeType], data(index, static_cast(MimeType)));
-                m.insert(names[Height], data(index, static_cast(Height)));
-                m.insert(names[Width], data(index, static_cast(Width)));
-                m.insert(names[ProportionalHeight],
-                         data(index, static_cast(ProportionalHeight)));
-                m.insert(names[Id], data(index, static_cast(Id)));
-                m.insert(names[State], data(index, static_cast(State)));
-                m.insert(names[IsEncrypted], data(index, static_cast(IsEncrypted)));
-                m.insert(names[ReplyTo], data(index, static_cast(ReplyTo)));
-                m.insert(names[RoomName], data(index, static_cast(RoomName)));
-                m.insert(names[RoomTopic], data(index, static_cast(RoomTopic)));
+                // m.insert(names[Section], data(id, static_cast(Section)));
+                m.insert(names[Type], data(id, static_cast(Type)));
+                m.insert(names[Body], data(id, static_cast(Body)));
+                m.insert(names[FormattedBody], data(id, static_cast(FormattedBody)));
+                m.insert(names[UserId], data(id, static_cast(UserId)));
+                m.insert(names[UserName], data(id, static_cast(UserName)));
+                m.insert(names[Timestamp], data(id, static_cast(Timestamp)));
+                m.insert(names[Url], data(id, static_cast(Url)));
+                m.insert(names[ThumbnailUrl], data(id, static_cast(ThumbnailUrl)));
+                m.insert(names[Filename], data(id, static_cast(Filename)));
+                m.insert(names[Filesize], data(id, static_cast(Filesize)));
+                m.insert(names[MimeType], data(id, static_cast(MimeType)));
+                m.insert(names[Height], data(id, static_cast(Height)));
+                m.insert(names[Width], data(id, static_cast(Width)));
+                m.insert(names[ProportionalHeight], data(id, static_cast(ProportionalHeight)));
+                m.insert(names[Id], data(id, static_cast(Id)));
+                m.insert(names[State], data(id, static_cast(State)));
+                m.insert(names[IsEncrypted], data(id, static_cast(IsEncrypted)));
+                m.insert(names[ReplyTo], data(id, static_cast(ReplyTo)));
+                m.insert(names[RoomName], data(id, static_cast(RoomName)));
+                m.insert(names[RoomTopic], data(id, static_cast(RoomTopic)));
 
                 return QVariant(m);
         }
@@ -359,6 +343,44 @@ TimelineModel::data(const QModelIndex &index, int role) const
         }
 }
 
+QVariant
+TimelineModel::data(const QModelIndex &index, int role) const
+{
+        using namespace mtx::accessors;
+        namespace acc = mtx::accessors;
+        if (index.row() < 0 && index.row() >= (int)eventOrder.size())
+                return QVariant();
+
+        QString id = eventOrder[index.row()];
+
+        mtx::events::collections::TimelineEvents event = events.value(id);
+
+        if (role == Section) {
+                QDateTime date = origin_server_ts(event);
+                date.setTime(QTime());
+
+                std::string userId = acc::sender(event);
+
+                for (size_t r = index.row() + 1; r < eventOrder.size(); r++) {
+                        auto tempEv        = events.value(eventOrder[r]);
+                        QDateTime prevDate = origin_server_ts(tempEv);
+                        prevDate.setTime(QTime());
+                        if (prevDate != date)
+                                return QString("%2 %1")
+                                  .arg(date.toMSecsSinceEpoch())
+                                  .arg(QString::fromStdString(userId));
+
+                        std::string prevUserId = acc::sender(tempEv);
+                        if (userId != prevUserId)
+                                break;
+                }
+
+                return QString("%1").arg(QString::fromStdString(userId));
+        }
+
+        return data(id, role);
+}
+
 bool
 TimelineModel::canFetchMore(const QModelIndex &) const
 {
@@ -515,6 +537,27 @@ TimelineModel::internalAddEvents(
 
                 this->events.insert(id, e);
                 ids.push_back(id);
+
+                auto replyTo  = mtx::accessors::in_reply_to_event(e);
+                auto qReplyTo = QString::fromStdString(replyTo);
+                if (!replyTo.empty() && !events.contains(qReplyTo)) {
+                        http::client()->get_event(
+                          this->room_id_.toStdString(),
+                          replyTo,
+                          [this, id, replyTo](
+                            const mtx::events::collections::TimelineEvents &timeline,
+                            mtx::http::RequestErr err) {
+                                  if (err) {
+                                          nhlog::net()->error(
+                                            "Failed to retrieve event with id {}, which was "
+                                            "requested to show the replyTo for event {}",
+                                            replyTo,
+                                            id.toStdString());
+                                          return;
+                                  }
+                                  emit replyFetched(id, timeline);
+                          });
+                }
         }
         return ids;
 }
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 61dd6b69..ae505c17 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -153,6 +153,7 @@ public:
         QHash roleNames() const override;
         int rowCount(const QModelIndex &parent = QModelIndex()) const override;
         QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+        QVariant data(const QString &id, int role) const;
 
         bool canFetchMore(const QModelIndex &) const override;
         void fetchMore(const QModelIndex &) override;
@@ -200,6 +201,7 @@ signals:
         void newMessageToSend(mtx::events::collections::TimelineEvents event);
         void mediaCached(QString mxcUrl, QString cacheUrl);
         void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
+        void replyFetched(QString requestingEvent, mtx::events::collections::TimelineEvents event);
 
 private:
         DecryptionResult decryptEvent(
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 74e09a33..8cb204a6 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -12,6 +12,8 @@
 #include "UserSettingsPage.h"
 #include "dialogs/ImageOverlay.h"
 
+Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
+
 void
 TimelineViewManager::updateColorPalette()
 {
@@ -59,6 +61,7 @@ TimelineViewManager::TimelineViewManager(QWidget *parent)
                                          "Can't instantiate enum!");
         qmlRegisterType("im.nheko", 1, 0, "DelegateChoice");
         qmlRegisterType("im.nheko", 1, 0, "DelegateChooser");
+        qRegisterMetaType();
 
 #ifdef USE_QUICK_VIEW
         view      = new QQuickView();
-- 
cgit 1.5.1


From fe912240bc1ab89b8a20ce87d5183f328f704d23 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 17 Jan 2020 01:25:14 +0100
Subject: Move typing display to qml

---
 CMakeLists.txt                       |  2 -
 resources/langs/nheko_de.ts          | 67 ++++++++++++++--------------
 resources/langs/nheko_el.ts          | 67 ++++++++++++++--------------
 resources/langs/nheko_en.ts          | 67 ++++++++++++++--------------
 resources/langs/nheko_fi.ts          | 67 ++++++++++++++--------------
 resources/langs/nheko_fr.ts          | 67 ++++++++++++++--------------
 resources/langs/nheko_nl.ts          | 67 ++++++++++++++--------------
 resources/langs/nheko_pl.ts          | 69 ++++++++++++++---------------
 resources/langs/nheko_ru.ts          | 69 ++++++++++++++---------------
 resources/langs/nheko_zh_CN.ts       | 65 +++++++++++++--------------
 resources/qml/TimelineView.qml       | 31 ++++++++++++-
 src/ChatPage.cpp                     | 53 ----------------------
 src/ChatPage.h                       |  9 ----
 src/TypingDisplay.cpp                | 86 ------------------------------------
 src/TypingDisplay.h                  | 36 ---------------
 src/timeline/TimelineModel.cpp       | 30 +++++++++++++
 src/timeline/TimelineModel.h         | 13 ++++++
 src/timeline/TimelineViewManager.cpp | 18 ++++++--
 18 files changed, 377 insertions(+), 506 deletions(-)
 delete mode 100644 src/TypingDisplay.cpp
 delete mode 100644 src/TypingDisplay.h

(limited to 'src/timeline/TimelineModel.h')

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7871bb6a..4ef6f378 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -242,7 +242,6 @@ set(SRC_FILES
     src/TextInputWidget.cpp
     src/TopRoomBar.cpp
     src/TrayIcon.cpp
-    src/TypingDisplay.cpp
     src/Utils.cpp
     src/UserInfoWidget.cpp
     src/UserSettingsPage.cpp
@@ -379,7 +378,6 @@ qt5_wrap_cpp(MOC_HEADERS
     src/TextInputWidget.h
     src/TopRoomBar.h
     src/TrayIcon.h
-    src/TypingDisplay.h
     src/UserInfoWidget.h
     src/UserSettingsPage.h
     src/WelcomePage.h
diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts
index fbc2aa4f..a6d0e98d 100644
--- a/resources/langs/nheko_de.ts
+++ b/resources/langs/nheko_de.ts
@@ -4,12 +4,12 @@
 
     ChatPage
     
-        
+        
         Failed to upload media. Please try again.
         Medienupload fehlgeschlagen. Bitte versuche es erneut.
     
     
-        
+        
         Failed to restore OLM account. Please login again.
         Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein.
     
@@ -19,7 +19,7 @@
         Gespeicherte Nachrichten konnten nicht wiederhergestellt werden. Bitte melde Dich erneut an.
     
     
-        
+        
         Failed to setup encryption keys. Server response: %1 %2. Please try again later.
         Fehler beim Setup der Verschlüsselungsschlüssel. Servermeldung: %1 %2. Bitte versuche es später erneut.
     
@@ -72,7 +72,7 @@
 
     EditModal
     
-        
+        
         Apply
         Anwenden
     
@@ -182,7 +182,7 @@
 
     MessageDelegate
     
-        
+        
         redacted
         gelöscht
     
@@ -223,7 +223,7 @@
 
     QuickSwitcher
     
-        
+        
         Search for a room...
         Raum suchen…
     
@@ -279,7 +279,7 @@
 
     ReplyPopup
     
-        
+        
         Logout
         Abmelden
     
@@ -287,7 +287,7 @@
 
     RoomInfo
     
-        
+        
         no version stored
         keine Version gespeichert
     
@@ -300,12 +300,12 @@
         Raum verlassen
     
     
-        
+        
         Accept
         Akzeptieren
     
     
-        
+        
         Decline
         Ablehnen
     
@@ -364,13 +364,13 @@
 
     TextInputWidget
     
-        
+        
         Send a file
         Versende Datei
     
     
         
-        
+        
         Write a message...
         Schreibe eine Nachricht…
     
@@ -385,7 +385,7 @@
         Emoji
     
     
-        
+        
         Select a file
         Datei auswählen
     
@@ -403,7 +403,7 @@
 
     TimelineModel
     
-        
+        
         -- Encrypted Event (No keys found for decryption) --
         Placeholder, when the message was not decrypted yet or can't be decrypted
         -- verschlüsselter Event (keine Schlüssel zur Entschlüsselung gefunden) --
@@ -427,18 +427,18 @@
         -- Entschlüsselungsfehler (%1) --
     
     
-        
+        
         -- Encrypted Event (Unknown event type) --
         Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet
         -- verschlüsselter Event (Unbekannter Eventtyp) --
     
     
-        
+        
         Message redaction failed: %1
         Nachricht zurückziehen fehlgeschlagen: %1
     
     
-        
+        
         Save image
         Bild speichern
     
@@ -457,11 +457,20 @@
         Save file
         Datei speichern
     
+    
+        
+        %1 and %2 are typing
+        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
+        
+            %1%2 tippt
+            %1 und %2 tippen
+        
+    
 
 
     TimelineRow
     
-        
+        
         Reply
         Antworten
     
@@ -550,18 +559,6 @@
         Schließen
     
 
-
-    TypingDisplay
-    
-        
-        %1 and %2 are typing
-        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
-        
-            %1%2 tippt
-            %1 und %2 tippen
-        
-    
-
 
     UserInfoWidget
     
@@ -573,7 +570,7 @@
 
     UserSettingsPage
     
-        
+        
         Minimize to tray
         Ins Benachrichtigungsfeld minimieren
     
@@ -740,7 +737,7 @@
 
     descriptiveTime
     
-        
+        
         Yesterday
         Gestern
     
@@ -1014,7 +1011,7 @@ Medien-Größe: %2
         Aktivierung der Verschlüsselung fehlgeschlagen: %1
     
     
-        
+        
         Select an avatar
         Wähle einen Avatar
     
@@ -1114,7 +1111,7 @@ Medien-Größe: %2
 
     message-description sent:
     
-        
+        
         You sent an audio clip
         Du hast eine Audiodatei gesendet.
     
@@ -1210,7 +1207,7 @@ Medien-Größe: %2
 
     utils
     
-        
+        
         sent a file.
         
     
diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts
index 1d9720ee..35fdeb80 100644
--- a/resources/langs/nheko_el.ts
+++ b/resources/langs/nheko_el.ts
@@ -4,12 +4,12 @@
 
     ChatPage
     
-        
+        
         Failed to upload media. Please try again.
         
     
     
-        
+        
         Failed to restore OLM account. Please login again.
         
     
@@ -19,7 +19,7 @@
         
     
     
-        
+        
         Failed to setup encryption keys. Server response: %1 %2. Please try again later.
         
     
@@ -72,7 +72,7 @@
 
     EditModal
     
-        
+        
         Apply
         
     
@@ -182,7 +182,7 @@
 
     MessageDelegate
     
-        
+        
         redacted
         
     
@@ -223,7 +223,7 @@
 
     QuickSwitcher
     
-        
+        
         Search for a room...
         Αναζήτηση συνομιλίας...
     
@@ -279,7 +279,7 @@
 
     ReplyPopup
     
-        
+        
         Logout
         
     
@@ -287,7 +287,7 @@
 
     RoomInfo
     
-        
+        
         no version stored
         
     
@@ -300,12 +300,12 @@
         Βγές
     
     
-        
+        
         Accept
         Αποδοχή
     
     
-        
+        
         Decline
         Απόρριψη
     
@@ -364,13 +364,13 @@
 
     TextInputWidget
     
-        
+        
         Send a file
         
     
     
         
-        
+        
         Write a message...
         Γράψε ένα μήνυμα...
     
@@ -385,7 +385,7 @@
         
     
     
-        
+        
         Select a file
         Διάλεξε ένα αρχείο
     
@@ -403,7 +403,7 @@
 
     TimelineModel
     
-        
+        
         -- Encrypted Event (No keys found for decryption) --
         Placeholder, when the message was not decrypted yet or can't be decrypted
         
@@ -427,18 +427,18 @@
         
     
     
-        
+        
         -- Encrypted Event (Unknown event type) --
         Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet
         
     
     
-        
+        
         Message redaction failed: %1
         
     
     
-        
+        
         Save image
         Αποθήκευση Εικόνας
     
@@ -457,11 +457,20 @@
         Save file
         
     
+    
+        
+        %1 and %2 are typing
+        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
+        
+            
+            
+        
+    
 
 
     TimelineRow
     
-        
+        
         Reply
         
     
@@ -550,18 +559,6 @@
         Έξοδος
     
 
-
-    TypingDisplay
-    
-        
-        %1 and %2 are typing
-        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
-        
-            
-            
-        
-    
-
 
     UserInfoWidget
     
@@ -573,7 +570,7 @@
 
     UserSettingsPage
     
-        
+        
         Minimize to tray
         Ελαχιστοποίηση
     
@@ -740,7 +737,7 @@
 
     descriptiveTime
     
-        
+        
         Yesterday
         
     
@@ -1012,7 +1009,7 @@ Media size: %2
         
     
     
-        
+        
         Select an avatar
         
     
@@ -1112,7 +1109,7 @@ Media size: %2
 
     message-description sent:
     
-        
+        
         You sent an audio clip
         
     
@@ -1208,7 +1205,7 @@ Media size: %2
 
     utils
     
-        
+        
         sent a file.
         
     
diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts
index fd8b4b25..40b6f09b 100644
--- a/resources/langs/nheko_en.ts
+++ b/resources/langs/nheko_en.ts
@@ -4,12 +4,12 @@
 
     ChatPage
     
-        
+        
         Failed to upload media. Please try again.
         
     
     
-        
+        
         Failed to restore OLM account. Please login again.
         Failed to restore OLM account. Please login again.
     
@@ -19,7 +19,7 @@
         Failed to restore save data. Please login again.
     
     
-        
+        
         Failed to setup encryption keys. Server response: %1 %2. Please try again later.
         Failed to setup encryption keys. Server response: %1 %2. Please try again later.
     
@@ -72,7 +72,7 @@
 
     EditModal
     
-        
+        
         Apply
         Apply
     
@@ -182,7 +182,7 @@
 
     MessageDelegate
     
-        
+        
         redacted
         
     
@@ -223,7 +223,7 @@
 
     QuickSwitcher
     
-        
+        
         Search for a room...
         Search for a room…
     
@@ -279,7 +279,7 @@
 
     ReplyPopup
     
-        
+        
         Logout
         Logout
     
@@ -287,7 +287,7 @@
 
     RoomInfo
     
-        
+        
         no version stored
         no version stored
     
@@ -300,12 +300,12 @@
         Leave room
     
     
-        
+        
         Accept
         Accept
     
     
-        
+        
         Decline
         Decline
     
@@ -364,13 +364,13 @@
 
     TextInputWidget
     
-        
+        
         Send a file
         Send a file
     
     
         
-        
+        
         Write a message...
         Write a message…
     
@@ -385,7 +385,7 @@
         Emoji
     
     
-        
+        
         Select a file
         Select a file
     
@@ -403,7 +403,7 @@
 
     TimelineModel
     
-        
+        
         -- Encrypted Event (No keys found for decryption) --
         Placeholder, when the message was not decrypted yet or can't be decrypted
         -- Encrypted Event (No keys found for decryption) --
@@ -427,18 +427,18 @@
         -- Decryption Error (%1) --
     
     
-        
+        
         -- Encrypted Event (Unknown event type) --
         Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet
         -- Encrypted Event (Unknown event type) --
     
     
-        
+        
         Message redaction failed: %1
         Message redaction failed: %1
     
     
-        
+        
         Save image
         Save image
     
@@ -457,11 +457,20 @@
         Save file
         
     
+    
+        
+        %1 and %2 are typing
+        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
+        
+            %1%2 is typing
+            %1 and %2 are typing
+        
+    
 
 
     TimelineRow
     
-        
+        
         Reply
         
     
@@ -550,18 +559,6 @@
         Quit
     
 
-
-    TypingDisplay
-    
-        
-        %1 and %2 are typing
-        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
-        
-            %1%2 is typing
-            %1 and %2 are typing
-        
-    
-
 
     UserInfoWidget
     
@@ -573,7 +570,7 @@
 
     UserSettingsPage
     
-        
+        
         Minimize to tray
         Minimize to tray
     
@@ -740,7 +737,7 @@
 
     descriptiveTime
     
-        
+        
         Yesterday
         Yesterday
     
@@ -1016,7 +1013,7 @@ Media size: %2
         Failed to enable encryption: %1
     
     
-        
+        
         Select an avatar
         Select an avatar
     
@@ -1116,7 +1113,7 @@ Media size: %2
 
     message-description sent:
     
-        
+        
         You sent an audio clip
         
     
@@ -1212,7 +1209,7 @@ Media size: %2
 
     utils
     
-        
+        
         sent a file.
         sent a file.
     
diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts
index d851b879..cae5a109 100644
--- a/resources/langs/nheko_fi.ts
+++ b/resources/langs/nheko_fi.ts
@@ -4,12 +4,12 @@
 
     ChatPage
     
-        
+        
         Failed to upload media. Please try again.
         
     
     
-        
+        
         Failed to restore OLM account. Please login again.
         OLM-tilin palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.
     
@@ -19,7 +19,7 @@
         Tallennettujen tietojen palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.
     
     
-        
+        
         Failed to setup encryption keys. Server response: %1 %2. Please try again later.
         Salausavainten lähetys epäonnistui. Palvelimen vastaus: %1 %2. Ole hyvä ja yritä uudelleen myöhemmin.
     
@@ -72,7 +72,7 @@
 
     EditModal
     
-        
+        
         Apply
         Tallenna
     
@@ -182,7 +182,7 @@
 
     MessageDelegate
     
-        
+        
         redacted
         
     
@@ -223,7 +223,7 @@
 
     QuickSwitcher
     
-        
+        
         Search for a room...
         Etsi huonetta…
     
@@ -279,7 +279,7 @@
 
     ReplyPopup
     
-        
+        
         Logout
         Kirjaudu ulos
     
@@ -287,7 +287,7 @@
 
     RoomInfo
     
-        
+        
         no version stored
         ei tallennettua versiota
     
@@ -300,12 +300,12 @@
         Poistu huoneesta
     
     
-        
+        
         Accept
         Hyväksy
     
     
-        
+        
         Decline
         Hylkää
     
@@ -364,13 +364,13 @@
 
     TextInputWidget
     
-        
+        
         Send a file
         Lähetä tiedosto
     
     
         
-        
+        
         Write a message...
         Kirjoita viesti…
     
@@ -385,7 +385,7 @@
         Emoji
     
     
-        
+        
         Select a file
         Valitse tiedosto
     
@@ -403,7 +403,7 @@
 
     TimelineModel
     
-        
+        
         -- Encrypted Event (No keys found for decryption) --
         Placeholder, when the message was not decrypted yet or can't be decrypted
         -- Salattu viesti (salauksen purkuavaimia ei löydetty) --
@@ -427,18 +427,18 @@
         -- Virhe purkaessa salausta (%1) --
     
     
-        
+        
         -- Encrypted Event (Unknown event type) --
         Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet
         -- Salattu viesti (tuntematon viestityyppi) --
     
     
-        
+        
         Message redaction failed: %1
         Viestin poisto epäonnistui: %1
     
     
-        
+        
         Save image
         Tallenna kuva
     
@@ -457,11 +457,20 @@
         Save file
         
     
+    
+        
+        %1 and %2 are typing
+        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
+        
+            %1%2 kirjoittaa
+            %1 ja %2 kirjoittavat
+        
+    
 
 
     TimelineRow
     
-        
+        
         Reply
         
     
@@ -550,18 +559,6 @@
         Lopeta
     
 
-
-    TypingDisplay
-    
-        
-        %1 and %2 are typing
-        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
-        
-            %1%2 kirjoittaa
-            %1 ja %2 kirjoittavat
-        
-    
-
 
     UserInfoWidget
     
@@ -573,7 +570,7 @@
 
     UserSettingsPage
     
-        
+        
         Minimize to tray
         Pienennä ilmoitusalueelle
     
@@ -740,7 +737,7 @@
 
     descriptiveTime
     
-        
+        
         Yesterday
         Eilen
     
@@ -1016,7 +1013,7 @@ Median koko: %2
         Salauksen aktivointi epäonnistui: %1
     
     
-        
+        
         Select an avatar
         Valitse profiilikuva
     
@@ -1116,7 +1113,7 @@ Median koko: %2
 
     message-description sent:
     
-        
+        
         You sent an audio clip
         
     
@@ -1212,7 +1209,7 @@ Median koko: %2
 
     utils
     
-        
+        
         sent a file.
         
     
diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts
index 15494953..3646d061 100644
--- a/resources/langs/nheko_fr.ts
+++ b/resources/langs/nheko_fr.ts
@@ -4,12 +4,12 @@
 
     ChatPage
     
-        
+        
         Failed to upload media. Please try again.
         
     
     
-        
+        
         Failed to restore OLM account. Please login again.
         
     
@@ -19,7 +19,7 @@
         
     
     
-        
+        
         Failed to setup encryption keys. Server response: %1 %2. Please try again later.
         
     
@@ -72,7 +72,7 @@
 
     EditModal
     
-        
+        
         Apply
         
     
@@ -182,7 +182,7 @@
 
     MessageDelegate
     
-        
+        
         redacted
         
     
@@ -223,7 +223,7 @@
 
     QuickSwitcher
     
-        
+        
         Search for a room...
         Chercher un salon…
     
@@ -280,7 +280,7 @@
 
     ReplyPopup
     
-        
+        
         Logout
         
     
@@ -288,7 +288,7 @@
 
     RoomInfo
     
-        
+        
         no version stored
         
     
@@ -301,12 +301,12 @@
         Quitter le salon
     
     
-        
+        
         Accept
         Accepter
     
     
-        
+        
         Decline
         Décliner
     
@@ -365,13 +365,13 @@
 
     TextInputWidget
     
-        
+        
         Send a file
         
     
     
         
-        
+        
         Write a message...
         Écrivez un message...
     
@@ -386,7 +386,7 @@
         
     
     
-        
+        
         Select a file
         Sélectionnez un fichier
     
@@ -404,7 +404,7 @@
 
     TimelineModel
     
-        
+        
         -- Encrypted Event (No keys found for decryption) --
         Placeholder, when the message was not decrypted yet or can't be decrypted
         
@@ -428,18 +428,18 @@
         
     
     
-        
+        
         -- Encrypted Event (Unknown event type) --
         Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet
         
     
     
-        
+        
         Message redaction failed: %1
         
     
     
-        
+        
         Save image
         Enregistrer l'image
     
@@ -458,11 +458,20 @@
         Save file
         
     
+    
+        
+        %1 and %2 are typing
+        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
+        
+            
+            
+        
+    
 
 
     TimelineRow
     
-        
+        
         Reply
         
     
@@ -551,18 +560,6 @@
         Quitter
     
 
-
-    TypingDisplay
-    
-        
-        %1 and %2 are typing
-        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
-        
-            
-            
-        
-    
-
 
     UserInfoWidget
     
@@ -574,7 +571,7 @@
 
     UserSettingsPage
     
-        
+        
         Minimize to tray
         Réduire à la barre des tâches
     
@@ -741,7 +738,7 @@
 
     descriptiveTime
     
-        
+        
         Yesterday
         
     
@@ -1015,7 +1012,7 @@ Taille du média : %2
         
     
     
-        
+        
         Select an avatar
         
     
@@ -1115,7 +1112,7 @@ Taille du média : %2
 
     message-description sent:
     
-        
+        
         You sent an audio clip
         
     
@@ -1211,7 +1208,7 @@ Taille du média : %2
 
     utils
     
-        
+        
         sent a file.
         
     
diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts
index 8ff6d5d8..960feb6e 100644
--- a/resources/langs/nheko_nl.ts
+++ b/resources/langs/nheko_nl.ts
@@ -4,12 +4,12 @@
 
     ChatPage
     
-        
+        
         Failed to upload media. Please try again.
         
     
     
-        
+        
         Failed to restore OLM account. Please login again.
         
     
@@ -19,7 +19,7 @@
         
     
     
-        
+        
         Failed to setup encryption keys. Server response: %1 %2. Please try again later.
         
     
@@ -72,7 +72,7 @@
 
     EditModal
     
-        
+        
         Apply
         
     
@@ -182,7 +182,7 @@
 
     MessageDelegate
     
-        
+        
         redacted
         
     
@@ -223,7 +223,7 @@
 
     QuickSwitcher
     
-        
+        
         Search for a room...
         Zoek een kamer...
     
@@ -279,7 +279,7 @@
 
     ReplyPopup
     
-        
+        
         Logout
         
     
@@ -287,7 +287,7 @@
 
     RoomInfo
     
-        
+        
         no version stored
         
     
@@ -300,12 +300,12 @@
         Kamer verlaten
     
     
-        
+        
         Accept
         Accepteren
     
     
-        
+        
         Decline
         Afwijzen
     
@@ -364,13 +364,13 @@
 
     TextInputWidget
     
-        
+        
         Send a file
         
     
     
         
-        
+        
         Write a message...
         Typ een bericht...
     
@@ -385,7 +385,7 @@
         
     
     
-        
+        
         Select a file
         Kies een bestand
     
@@ -403,7 +403,7 @@
 
     TimelineModel
     
-        
+        
         -- Encrypted Event (No keys found for decryption) --
         Placeholder, when the message was not decrypted yet or can't be decrypted
         
@@ -427,18 +427,18 @@
         
     
     
-        
+        
         -- Encrypted Event (Unknown event type) --
         Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet
         
     
     
-        
+        
         Message redaction failed: %1
         
     
     
-        
+        
         Save image
         Afbeelding opslaan
     
@@ -457,11 +457,20 @@
         Save file
         
     
+    
+        
+        %1 and %2 are typing
+        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
+        
+            
+            
+        
+    
 
 
     TimelineRow
     
-        
+        
         Reply
         
     
@@ -550,18 +559,6 @@
         Afsluiten
     
 
-
-    TypingDisplay
-    
-        
-        %1 and %2 are typing
-        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
-        
-            
-            
-        
-    
-
 
     UserInfoWidget
     
@@ -573,7 +570,7 @@
 
     UserSettingsPage
     
-        
+        
         Minimize to tray
         Minimaliseren naar systeemvak
     
@@ -740,7 +737,7 @@
 
     descriptiveTime
     
-        
+        
         Yesterday
         
     
@@ -1014,7 +1011,7 @@ Mediagrootte: %2
         
     
     
-        
+        
         Select an avatar
         
     
@@ -1114,7 +1111,7 @@ Mediagrootte: %2
 
     message-description sent:
     
-        
+        
         You sent an audio clip
         
     
@@ -1210,7 +1207,7 @@ Mediagrootte: %2
 
     utils
     
-        
+        
         sent a file.
         
     
diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts
index cb628fa6..a94ab30e 100644
--- a/resources/langs/nheko_pl.ts
+++ b/resources/langs/nheko_pl.ts
@@ -4,12 +4,12 @@
 
     ChatPage
     
-        
+        
         Failed to upload media. Please try again.
         
     
     
-        
+        
         Failed to restore OLM account. Please login again.
         Nie udało się przywrócić konta OLM. Spróbuj zalogować się ponownie.
     
@@ -19,7 +19,7 @@
         Nie udało się przywrócić zapisanych danych. Spróbuj zalogować się ponownie.
     
     
-        
+        
         Failed to setup encryption keys. Server response: %1 %2. Please try again later.
         
     
@@ -72,7 +72,7 @@
 
     EditModal
     
-        
+        
         Apply
         
     
@@ -182,7 +182,7 @@
 
     MessageDelegate
     
-        
+        
         redacted
         
     
@@ -223,7 +223,7 @@
 
     QuickSwitcher
     
-        
+        
         Search for a room...
         Wyszukaj pokoju…
     
@@ -279,7 +279,7 @@
 
     ReplyPopup
     
-        
+        
         Logout
         Wyloguj
     
@@ -287,7 +287,7 @@
 
     RoomInfo
     
-        
+        
         no version stored
         
     
@@ -300,12 +300,12 @@
         Opuść pokój
     
     
-        
+        
         Accept
         Akceptuj
     
     
-        
+        
         Decline
         Odrzuć
     
@@ -364,13 +364,13 @@
 
     TextInputWidget
     
-        
+        
         Send a file
         Wyślij plik
     
     
         
-        
+        
         Write a message...
         Napisz wiadomość…
     
@@ -385,7 +385,7 @@
         Emoji
     
     
-        
+        
         Select a file
         Wybierz plik
     
@@ -403,7 +403,7 @@
 
     TimelineModel
     
-        
+        
         -- Encrypted Event (No keys found for decryption) --
         Placeholder, when the message was not decrypted yet or can't be decrypted
         
@@ -427,18 +427,18 @@
         
     
     
-        
+        
         -- Encrypted Event (Unknown event type) --
         Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet
         
     
     
-        
+        
         Message redaction failed: %1
         Redagowanie wiadomości nie powiodło się: %1
     
     
-        
+        
         Save image
         Zapisz obraz
     
@@ -457,11 +457,21 @@
         Save file
         
     
+    
+        
+        %1 and %2 are typing
+        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
+        
+            
+            
+            
+        
+    
 
 
     TimelineRow
     
-        
+        
         Reply
         
     
@@ -550,19 +560,6 @@
         Zakończ
     
 
-
-    TypingDisplay
-    
-        
-        %1 and %2 are typing
-        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
-        
-            
-            
-            
-        
-    
-
 
     UserInfoWidget
     
@@ -574,7 +571,7 @@
 
     UserSettingsPage
     
-        
+        
         Minimize to tray
         Zminimalizuj do paska zadań
     
@@ -741,7 +738,7 @@
 
     descriptiveTime
     
-        
+        
         Yesterday
         
     
@@ -1018,7 +1015,7 @@ Rozmiar multimediów: %2
         Nie udało się włączyć szyfrowania: %1
     
     
-        
+        
         Select an avatar
         Wybierz awatar
     
@@ -1118,7 +1115,7 @@ Rozmiar multimediów: %2
 
     message-description sent:
     
-        
+        
         You sent an audio clip
         
     
@@ -1214,7 +1211,7 @@ Rozmiar multimediów: %2
 
     utils
     
-        
+        
         sent a file.
         
     
diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts
index 12b9873e..e8c7a4e8 100644
--- a/resources/langs/nheko_ru.ts
+++ b/resources/langs/nheko_ru.ts
@@ -4,12 +4,12 @@
 
     ChatPage
     
-        
+        
         Failed to upload media. Please try again.
         
     
     
-        
+        
         Failed to restore OLM account. Please login again.
         Не удалось восстановить учетную запись OLM. Пожалуйста, войдите снова.
     
@@ -19,7 +19,7 @@
         Не удалось восстановить сохраненные данные. Пожалуйста, войдите снова.
     
     
-        
+        
         Failed to setup encryption keys. Server response: %1 %2. Please try again later.
         Не удалось настроить ключи шифрования. Ответ сервера:%1 %2. Пожалуйста, попробуйте позже.
     
@@ -72,7 +72,7 @@
 
     EditModal
     
-        
+        
         Apply
         Применить
     
@@ -182,7 +182,7 @@
 
     MessageDelegate
     
-        
+        
         redacted
         
     
@@ -223,7 +223,7 @@
 
     QuickSwitcher
     
-        
+        
         Search for a room...
         Поиск комнаты...
     
@@ -279,7 +279,7 @@
 
     ReplyPopup
     
-        
+        
         Logout
         Выйти
     
@@ -287,7 +287,7 @@
 
     RoomInfo
     
-        
+        
         no version stored
         
     
@@ -300,12 +300,12 @@
         Покинуть комнату
     
     
-        
+        
         Accept
         Принять
     
     
-        
+        
         Decline
         Отказаться
     
@@ -364,13 +364,13 @@
 
     TextInputWidget
     
-        
+        
         Send a file
         Отправить файл
     
     
         
-        
+        
         Write a message...
         Написать сообщение...
     
@@ -385,7 +385,7 @@
         
     
     
-        
+        
         Select a file
         Выберите файл
     
@@ -403,7 +403,7 @@
 
     TimelineModel
     
-        
+        
         -- Encrypted Event (No keys found for decryption) --
         Placeholder, when the message was not decrypted yet or can't be decrypted
         
@@ -427,18 +427,18 @@
         
     
     
-        
+        
         -- Encrypted Event (Unknown event type) --
         Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet
         
     
     
-        
+        
         Message redaction failed: %1
         Ошибка редактирования сообщения: %1
     
     
-        
+        
         Save image
         Сохранить изображение
     
@@ -457,11 +457,21 @@
         Save file
         
     
+    
+        
+        %1 and %2 are typing
+        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
+        
+            
+            
+            
+        
+    
 
 
     TimelineRow
     
-        
+        
         Reply
         
     
@@ -550,19 +560,6 @@
         Выйти
     
 
-
-    TypingDisplay
-    
-        
-        %1 and %2 are typing
-        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
-        
-            
-            
-            
-        
-    
-
 
     UserInfoWidget
     
@@ -574,7 +571,7 @@
 
     UserSettingsPage
     
-        
+        
         Minimize to tray
         Сворачивать в системную панель
     
@@ -742,7 +739,7 @@
 
     descriptiveTime
     
-        
+        
         Yesterday
         
     
@@ -1017,7 +1014,7 @@ Media size: %2
         Не удалось включить шифрование: %1
     
     
-        
+        
         Select an avatar
         Выберите аватар
     
@@ -1117,7 +1114,7 @@ Media size: %2
 
     message-description sent:
     
-        
+        
         You sent an audio clip
         
     
@@ -1213,7 +1210,7 @@ Media size: %2
 
     utils
     
-        
+        
         sent a file.
         
     
diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts
index 799a534d..718bcb55 100644
--- a/resources/langs/nheko_zh_CN.ts
+++ b/resources/langs/nheko_zh_CN.ts
@@ -4,12 +4,12 @@
 
     ChatPage
     
-        
+        
         Failed to upload media. Please try again.
         
     
     
-        
+        
         Failed to restore OLM account. Please login again.
         恢复 OLM 账户失败。请重新登录。
     
@@ -19,7 +19,7 @@
         恢复保存的数据失败。请重新登录。
     
     
-        
+        
         Failed to setup encryption keys. Server response: %1 %2. Please try again later.
         
     
@@ -72,7 +72,7 @@
 
     EditModal
     
-        
+        
         Apply
         
     
@@ -182,7 +182,7 @@
 
     MessageDelegate
     
-        
+        
         redacted
         
     
@@ -223,7 +223,7 @@
 
     QuickSwitcher
     
-        
+        
         Search for a room...
         寻找一个聊天室...
     
@@ -279,7 +279,7 @@
 
     ReplyPopup
     
-        
+        
         Logout
         登出
     
@@ -287,7 +287,7 @@
 
     RoomInfo
     
-        
+        
         no version stored
         
     
@@ -300,12 +300,12 @@
         离开聊天室
     
     
-        
+        
         Accept
         接受
     
     
-        
+        
         Decline
         拒绝
     
@@ -364,13 +364,13 @@
 
     TextInputWidget
     
-        
+        
         Send a file
         发送一个文件
     
     
         
-        
+        
         Write a message...
         写一条消息...
     
@@ -385,7 +385,7 @@
         
     
     
-        
+        
         Select a file
         选择一个文件
     
@@ -403,7 +403,7 @@
 
     TimelineModel
     
-        
+        
         -- Encrypted Event (No keys found for decryption) --
         Placeholder, when the message was not decrypted yet or can't be decrypted
         
@@ -427,18 +427,18 @@
         
     
     
-        
+        
         -- Encrypted Event (Unknown event type) --
         Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet
         
     
     
-        
+        
         Message redaction failed: %1
         删除消息失败:%1
     
     
-        
+        
         Save image
         保存图像
     
@@ -457,11 +457,19 @@
         Save file
         
     
+    
+        
+        %1 and %2 are typing
+        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
+        
+            
+        
+    
 
 
     TimelineRow
     
-        
+        
         Reply
         
     
@@ -550,17 +558,6 @@
         退出
     
 
-
-    TypingDisplay
-    
-        
-        %1 and %2 are typing
-        Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)
-        
-            
-        
-    
-
 
     UserInfoWidget
     
@@ -572,7 +569,7 @@
 
     UserSettingsPage
     
-        
+        
         Minimize to tray
         最小化至托盘
     
@@ -739,7 +736,7 @@
 
     descriptiveTime
     
-        
+        
         Yesterday
         
     
@@ -1014,7 +1011,7 @@ Media size: %2
         启用加密失败:%1
     
     
-        
+        
         Select an avatar
         选择一个头像
     
@@ -1122,7 +1119,7 @@ Media size: %2
 
     message-description sent:
     
-        
+        
         You sent an audio clip
         
     
@@ -1218,7 +1215,7 @@ Media size: %2
 
     utils
     
-        
+        
         sent a file.
         
     
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index bfea79ee..60a8e6c1 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -38,7 +38,11 @@ Item {
 			id: chat
 
 			visible: timelineManager.timeline != null
-			anchors.fill: parent
+
+			anchors.left: parent.left
+			anchors.right: parent.right
+			anchors.top: parent.top
+			anchors.bottom: chatFooter.top
 
 			anchors.leftMargin: 4
 			anchors.rightMargin: scrollbar.width
@@ -179,6 +183,31 @@ Item {
 					}
 				}
 			}
+
+		}
+
+		Rectangle {
+			id: chatFooter
+
+			height: Math.max(16, typingDisplay.height)
+			anchors.left: parent.left
+			anchors.right: parent.right
+			anchors.bottom: parent.bottom
+			z: 3
+
+			color: colors.window
+
+			Text {
+				anchors.left: parent.left
+				anchors.right: parent.right
+				anchors.bottom: parent.bottom
+				anchors.leftMargin: 10
+				anchors.rightMargin: 10
+
+				id: typingDisplay
+				text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, chatFooter.color) : ""
+				color: colors.windowText
+			}
 		}
 	}
 }
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 777709be..125e229a 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -34,7 +34,6 @@
 #include "Splitter.h"
 #include "TextInputWidget.h"
 #include "TopRoomBar.h"
-#include "TypingDisplay.h"
 #include "UserInfoWidget.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
@@ -130,11 +129,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
         text_input_ = new TextInputWidget(this);
         contentLayout_->addWidget(text_input_);
 
-        typingDisplay_ = new TypingDisplay(content_);
-        typingDisplay_->hide();
-        connect(
-          text_input_, &TextInputWidget::heightChanged, typingDisplay_, &TypingDisplay::setOffset);
-
         typingRefresher_ = new QTimer(this);
         typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT);
 
@@ -225,19 +219,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 }
         });
 
-        connect(room_list_, &RoomList::roomChanged, this, [this](const QString &roomid) {
-                QStringList users;
-
-                if (!userSettings_->isTypingNotificationsEnabled()) {
-                        typingDisplay_->setUsers(users);
-                        return;
-                }
-
-                if (typingUsers_.find(roomid) != typingUsers_.end())
-                        users = typingUsers_[roomid];
-
-                typingDisplay_->setUsers(users);
-        });
         connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping);
         connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo);
         connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView);
@@ -472,8 +453,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 bool hasNotifications = false;
                 for (const auto &room : rooms.join) {
                         auto room_id = QString::fromStdString(room.first);
-
-                        updateTypingUsers(room_id, room.second.ephemeral.typing);
                         updateRoomNotificationCount(
                           room_id,
                           room.second.unread_notifications.notification_count,
@@ -786,38 +765,6 @@ ChatPage::removeRoom(const QString &room_id)
         room_list_->removeRoom(room_id, room_id == current_room_);
 }
 
-void
-ChatPage::updateTypingUsers(const QString &roomid, const std::vector &user_ids)
-{
-        if (!userSettings_->isTypingNotificationsEnabled())
-                return;
-
-        typingUsers_[roomid] = generateTypingUsers(roomid, user_ids);
-
-        if (current_room_ == roomid)
-                typingDisplay_->setUsers(typingUsers_[roomid]);
-}
-
-QStringList
-ChatPage::generateTypingUsers(const QString &room_id, const std::vector &typing_users)
-{
-        QStringList users;
-        auto local_user = utils::localUser();
-
-        for (const auto &uid : typing_users) {
-                const auto remote_user = QString::fromStdString(uid);
-
-                if (remote_user == local_user)
-                        continue;
-
-                users.append(cache::displayName(room_id, remote_user));
-        }
-
-        users.sort();
-
-        return users;
-}
-
 void
 ChatPage::removeLeftRooms(const std::map &rooms)
 {
diff --git a/src/ChatPage.h b/src/ChatPage.h
index e4c0ef16..354a21f3 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -48,7 +48,6 @@ class Splitter;
 class TextInputWidget;
 class TimelineViewManager;
 class TopRoomBar;
-class TypingDisplay;
 class UserInfoWidget;
 class UserSettings;
 class NotificationsManager;
@@ -187,8 +186,6 @@ private:
         using LeftRooms = std::map;
         void removeLeftRooms(const LeftRooms &rooms);
 
-        void updateTypingUsers(const QString &roomid, const std::vector &user_ids);
-
         void loadStateFromCache();
         void resetUI();
         //! Decides whether or not to hide the group's sidebar.
@@ -206,9 +203,6 @@ private:
 
         void showNotificationsDialog(const QPoint &point);
 
-        QStringList generateTypingUsers(const QString &room_id,
-                                        const std::vector &typing_users);
-
         QHBoxLayout *topLayout_;
         Splitter *splitter;
 
@@ -228,7 +222,6 @@ private:
 
         TopRoomBar *top_bar_;
         TextInputWidget *text_input_;
-        TypingDisplay *typingDisplay_;
 
         QTimer connectivityTimer_;
         std::atomic_bool isConnected_;
@@ -240,8 +233,6 @@ private:
 
         popups::UserMentions *user_mentions_popup_;
 
-        // Keeps track of the users currently typing on each room.
-        std::map> typingUsers_;
         QTimer *typingRefresher_;
 
         // Global user settings.
diff --git a/src/TypingDisplay.cpp b/src/TypingDisplay.cpp
deleted file mode 100644
index 43fabcd8..00000000
--- a/src/TypingDisplay.cpp
+++ /dev/null
@@ -1,86 +0,0 @@
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "Config.h"
-#include "TypingDisplay.h"
-#include "ui/Painter.h"
-
-constexpr int LEFT_PADDING = 24;
-constexpr int RECT_PADDING = 2;
-
-TypingDisplay::TypingDisplay(QWidget *parent)
-  : OverlayWidget(parent)
-  , offset_{conf::textInput::height}
-{
-        setFixedHeight(QFontMetrics(font()).height() + RECT_PADDING);
-        setAttribute(Qt::WA_TransparentForMouseEvents);
-}
-
-void
-TypingDisplay::setOffset(int margin)
-{
-        offset_ = margin;
-        move(0, parentWidget()->height() - offset_ - height());
-}
-
-void
-TypingDisplay::setUsers(const QStringList &uid)
-{
-        move(0, parentWidget()->height() - offset_ - height());
-
-        text_.clear();
-
-        QString temp = text_ +=
-          tr("%1 and %2 are typing",
-             "Multiple users are typing. First argument is a comma separated list of potentially "
-             "multiple users. Second argument is the last user of that list. (If only one user is "
-             "typing, %1 is empty. You should still use it in your string though to silence Qt "
-             "warnings.)",
-             uid.size());
-
-        if (uid.isEmpty()) {
-                hide();
-                update();
-
-                return;
-        }
-
-        QStringList uidWithoutLast = uid;
-        uidWithoutLast.pop_back();
-        text_ = temp.arg(uidWithoutLast.join(", ")).arg(uid.back());
-
-        show();
-        update();
-}
-
-void
-TypingDisplay::paintEvent(QPaintEvent *)
-{
-        Painter p(this);
-        PainterHighQualityEnabler hq(p);
-
-        QFont f;
-        f.setPointSizeF(f.pointSizeF() * 0.9);
-
-        p.setFont(f);
-        p.setPen(QPen(textColor()));
-
-        QRect region = rect();
-        region.translate(LEFT_PADDING, 0);
-
-        QFontMetrics fm(f);
-        text_ = fm.elidedText(text_, Qt::ElideRight, (double)(width() * 0.75));
-
-        QPainterPath path;
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-        path.addRoundedRect(QRectF(0, 0, fm.width(text_) + 2 * LEFT_PADDING, height()), 3, 3);
-#else
-        path.addRoundedRect(
-          QRectF(0, 0, fm.horizontalAdvance(text_) + 2 * LEFT_PADDING, height()), 3, 3);
-#endif
-        p.fillPath(path, backgroundColor());
-        p.drawText(region, Qt::AlignVCenter, text_);
-}
diff --git a/src/TypingDisplay.h b/src/TypingDisplay.h
deleted file mode 100644
index 332d9c66..00000000
--- a/src/TypingDisplay.h
+++ /dev/null
@@ -1,36 +0,0 @@
-#pragma once
-
-#include "ui/OverlayWidget.h"
-
-class QPaintEvent;
-
-class TypingDisplay : public OverlayWidget
-{
-        Q_OBJECT
-
-        Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
-        Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
-
-public:
-        TypingDisplay(QWidget *parent = nullptr);
-
-        void setUsers(const QStringList &user_ids);
-
-        void setTextColor(const QColor &color) { textColor_ = color; };
-        QColor textColor() const { return textColor_; };
-
-        void setBackgroundColor(const QColor &color) { bgColor_ = color; };
-        QColor backgroundColor() const { return bgColor_; };
-
-public slots:
-        void setOffset(int margin);
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-
-private:
-        int offset_;
-        QColor textColor_;
-        QColor bgColor_;
-        QString text_;
-};
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 08c29927..2fd4b6d4 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -1388,3 +1388,33 @@ TimelineModel::cacheMedia(QString eventId)
                   emit mediaCached(mxcUrl, filename.filePath());
           });
 }
+
+QString
+TimelineModel::formatTypingUsers(const std::vector &users, QColor bg)
+{
+        QString temp =
+          tr("%1 and %2 are typing",
+             "Multiple users are typing. First argument is a comma separated list of potentially "
+             "multiple users. Second argument is the last user of that list. (If only one user is "
+             "typing, %1 is empty. You should still use it in your string though to silence Qt "
+             "warnings.)",
+             users.size());
+
+        if (users.empty()) {
+                return "";
+        }
+
+        QStringList uidWithoutLast;
+
+        auto formatUser = [this, bg](const QString &user_id) -> QString {
+                return QString("%2")
+                  .arg(userColor(user_id, bg).name())
+                  .arg(escapeEmoji(displayName(user_id).toHtmlEscaped()));
+        };
+
+        for (size_t i = 0; i + 1 < users.size(); i++) {
+                uidWithoutLast.append(formatUser(users[i]));
+        }
+
+        return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back()));
+}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index ae505c17..6d351359 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -120,6 +120,8 @@ class TimelineModel : public QAbstractListModel
         Q_OBJECT
         Q_PROPERTY(
           int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
+        Q_PROPERTY(std::vector typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY
+                     typingUsersChanged)
 
 public:
         explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0);
@@ -162,6 +164,7 @@ public:
         Q_INVOKABLE QString displayName(QString id) const;
         Q_INVOKABLE QString avatarUrl(QString id) const;
         Q_INVOKABLE QString formatDateSeparator(QDate date) const;
+        Q_INVOKABLE QString formatTypingUsers(const std::vector &users, QColor bg);
 
         Q_INVOKABLE QString escapeEmoji(QString str) const;
         Q_INVOKABLE void viewRawMessage(QString id) const;
@@ -183,6 +186,14 @@ public slots:
         int currentIndex() const { return idToIndex(currentId); }
         void markEventsAsRead(const std::vector &event_ids);
         QVariantMap getDump(QString eventId) const;
+        void updateTypingUsers(const std::vector &users)
+        {
+                if (this->typingUsers_ != users) {
+                        this->typingUsers_ = users;
+                        emit typingUsersChanged(typingUsers_);
+                }
+        }
+        std::vector typingUsers() const { return typingUsers_; }
 
 private slots:
         // Add old events at the top of the timeline.
@@ -202,6 +213,7 @@ signals:
         void mediaCached(QString mxcUrl, QString cacheUrl);
         void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
         void replyFetched(QString requestingEvent, mtx::events::collections::TimelineEvents event);
+        void typingUsersChanged(std::vector users);
 
 private:
         DecryptionResult decryptEvent(
@@ -232,6 +244,7 @@ private:
 
         QHash userColors;
         QString currentId;
+        std::vector typingUsers_;
 
         TimelineViewManager *manager_;
 
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index ddbc6af8..c7a4e50e 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -8,6 +8,7 @@
 #include "ColorImageProvider.h"
 #include "DelegateChooser.h"
 #include "Logging.h"
+#include "MatrixClient.h"
 #include "MxcImageProvider.h"
 #include "UserSettingsPage.h"
 #include "dialogs/ImageOverlay.h"
@@ -92,10 +93,21 @@ TimelineViewManager::TimelineViewManager(QWidget *parent)
 void
 TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
 {
-        for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
+        for (const auto &[room_id, room] : rooms.join) {
                 // addRoom will only add the room, if it doesn't exist
-                addRoom(QString::fromStdString(it->first));
-                models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline);
+                addRoom(QString::fromStdString(room_id));
+                const auto &room_model = models.value(QString::fromStdString(room_id));
+                room_model->addEvents(room.timeline);
+
+                if (ChatPage::instance()->userSettings()->isTypingNotificationsEnabled()) {
+                        std::vector typing;
+                        typing.reserve(room.ephemeral.typing.size());
+                        for (const auto &user : room.ephemeral.typing) {
+                                if (user != http::client()->user_id().to_string())
+                                        typing.push_back(QString::fromStdString(user));
+                        }
+                        room_model->updateTypingUsers(typing);
+                }
         }
 
         this->isInitialSync_ = false;
-- 
cgit 1.5.1


From 86960e67ec93f9c739540201814f09c6277bbff8 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Thu, 23 Jan 2020 20:34:04 +0100
Subject: Implement display of membership events

---
 resources/qml/delegates/MessageDelegate.qml |   6 ++
 src/timeline/TimelineModel.cpp              | 102 +++++++++++++++++++++++++++-
 src/timeline/TimelineModel.h                |   3 +-
 3 files changed, 108 insertions(+), 3 deletions(-)

(limited to 'src/timeline/TimelineModel.h')

diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index ae18d505..512c790b 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -78,6 +78,12 @@ Item {
 				notice: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic")
 			}
 		}
+		DelegateChoice {
+			roleValue: MtxEvent.Member
+			NoticeMessage {
+				notice: timelineManager.timeline.formatMemberEvent(model.data.id);
+			}
+		}
 		DelegateChoice {
 			Placeholder {}
 		}
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 65a6e470..774e30da 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -189,7 +189,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
         connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage);
 
         connect(this,
-                &TimelineModel::replyFetched,
+                &TimelineModel::eventFetched,
                 this,
                 [this](QString requestingEvent, mtx::events::collections::TimelineEvents event) {
                         events.insert(QString::fromStdString(mtx::accessors::event_id(event)),
@@ -566,7 +566,7 @@ TimelineModel::internalAddEvents(
                                             id.toStdString());
                                           return;
                                   }
-                                  emit replyFetched(id, timeline);
+                                  emit eventFetched(id, timeline);
                           });
                 }
         }
@@ -1442,3 +1442,101 @@ TimelineModel::formatTypingUsers(const std::vector &users, QColor bg)
 
         return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back()));
 }
+
+QString
+TimelineModel::formatMemberEvent(QString id)
+{
+        if (!events.contains(id))
+                return "";
+
+        auto event = std::get_if>(&events[id]);
+        if (!event)
+                return "";
+
+        mtx::events::StateEvent *prevEvent = nullptr;
+        QString prevEventId = QString::fromStdString(event->unsigned_data.replaces_state);
+        if (!prevEventId.isEmpty()) {
+                if (!events.contains(prevEventId)) {
+                        http::client()->get_event(
+                          this->room_id_.toStdString(),
+                          event->unsigned_data.replaces_state,
+                          [this, id, prevEventId](
+                            const mtx::events::collections::TimelineEvents &timeline,
+                            mtx::http::RequestErr err) {
+                                  if (err) {
+                                          nhlog::net()->error(
+                                            "Failed to retrieve event with id {}, which was "
+                                            "requested to show the membership for event {}",
+                                            prevEventId.toStdString(),
+                                            id.toStdString());
+                                          return;
+                                  }
+                                  emit eventFetched(id, timeline);
+                          });
+                } else {
+                        prevEvent =
+                          std::get_if>(
+                            &events[prevEventId]);
+                }
+        }
+
+        QString user = QString::fromStdString(event->state_key);
+        QString name = escapeEmoji(displayName(user));
+
+        // see table https://matrix.org/docs/spec/client_server/latest#m-room-member
+        using namespace mtx::events::state;
+        switch (event->content.membership) {
+        case Membership::Invite:
+                return tr("%1 was invited.").arg(name);
+        case Membership::Join:
+                if (prevEvent && prevEvent->content.membership == Membership::Join) {
+                        bool displayNameChanged =
+                          prevEvent->content.display_name != event->content.display_name;
+                        bool avatarChanged =
+                          prevEvent->content.avatar_url != event->content.avatar_url;
+
+                        if (displayNameChanged && avatarChanged)
+                                return tr("%1 changed their display name and avatar.").arg(name);
+                        else if (displayNameChanged)
+                                return tr("%1 changed their display name.").arg(name);
+                        else if (avatarChanged)
+                                return tr("%1 changed their avatar.").arg(name);
+                        // the case of nothing changed but join follows join shouldn't happen, so
+                        // just show it as join
+                }
+                return tr("%1 joined.").arg(name);
+        case Membership::Leave:
+                if (!prevEvent) // Should only ever happen temporarily
+                        return "";
+
+                if (prevEvent->content.membership == Membership::Invite) {
+                        if (event->state_key == event->sender)
+                                return tr("%1 rejected their invite.").arg(name);
+                        else
+                                return tr("Revoked the invite to %1.").arg(name);
+                } else if (prevEvent->content.membership == Membership::Join) {
+                        if (event->state_key == event->sender)
+                                return tr("%1 left the room.").arg(name);
+                        else
+                                return tr("Kicked %1.").arg(name);
+                } else if (prevEvent->content.membership == Membership::Ban) {
+                        return tr("Unbanned %1").arg(name);
+                } else if (prevEvent->content.membership == Membership::Knock) {
+                        if (event->state_key == event->sender)
+                                return tr("%1 redacted their knock.").arg(name);
+                        else
+                                return tr("Rejected the knock from %1.").arg(name);
+                } else
+                        return tr("%1 left after having already left!",
+                                  "This is a leave event after the user already left and shouln't "
+                                  "happen apart from state resets")
+                          .arg(name);
+
+        case Membership::Ban:
+                return tr("%1 was banned.").arg(name);
+        case Membership::Knock:
+                return tr("%1 knocked.").arg(name);
+        default:
+                return "";
+        }
+}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 6d351359..52ab28cf 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -165,6 +165,7 @@ public:
         Q_INVOKABLE QString avatarUrl(QString id) const;
         Q_INVOKABLE QString formatDateSeparator(QDate date) const;
         Q_INVOKABLE QString formatTypingUsers(const std::vector &users, QColor bg);
+        Q_INVOKABLE QString formatMemberEvent(QString id);
 
         Q_INVOKABLE QString escapeEmoji(QString str) const;
         Q_INVOKABLE void viewRawMessage(QString id) const;
@@ -212,7 +213,7 @@ signals:
         void newMessageToSend(mtx::events::collections::TimelineEvents event);
         void mediaCached(QString mxcUrl, QString cacheUrl);
         void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
-        void replyFetched(QString requestingEvent, mtx::events::collections::TimelineEvents event);
+        void eventFetched(QString requestingEvent, mtx::events::collections::TimelineEvents event);
         void typingUsersChanged(std::vector users);
 
 private:
-- 
cgit 1.5.1


From e9267ffc7617cfbeb26c62cde3c28f33a898161c Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Thu, 23 Jan 2020 20:59:17 +0100
Subject: Show event type string in placeholder

---
 resources/qml/delegates/Placeholder.qml |  2 +-
 src/timeline/TimelineModel.cpp          | 11 +++++++++++
 src/timeline/TimelineModel.h            |  1 +
 3 files changed, 13 insertions(+), 1 deletion(-)

(limited to 'src/timeline/TimelineModel.h')

diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml
index 36d7b2bc..26de2067 100644
--- a/resources/qml/delegates/Placeholder.qml
+++ b/resources/qml/delegates/Placeholder.qml
@@ -1,7 +1,7 @@
 import ".."
 
 MatrixText {
-	text: qsTr("unimplemented event: ") + model.data.type
+	text: qsTr("unimplemented event: ") + model.data.typeString
 	width: parent ? parent.width : undefined
 	color: inactiveColors.text
 }
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 774e30da..f66099a0 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -119,6 +119,13 @@ toRoomEventType(const mtx::events::collections::TimelineEvents &event)
         return std::visit(RoomEventType{}, event);
 }
 
+QString
+toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event)
+{
+        return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); },
+                          event);
+}
+
 TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
   : QAbstractListModel(parent)
   , room_id_(room_id)
@@ -206,6 +213,7 @@ TimelineModel::roleNames() const
         return {
           {Section, "section"},
           {Type, "type"},
+          {TypeString, "typeString"},
           {Body, "body"},
           {FormattedBody, "formattedBody"},
           {UserId, "userId"},
@@ -265,6 +273,8 @@ TimelineModel::data(const QString &id, int role) const
                 return QVariant(origin_server_ts(event));
         case Type:
                 return QVariant(toRoomEventType(event));
+        case TypeString:
+                return QVariant(toRoomEventTypeString(event));
         case Body:
                 return QVariant(utils::replaceEmoji(QString::fromStdString(body(event))));
         case FormattedBody: {
@@ -327,6 +337,7 @@ TimelineModel::data(const QString &id, int role) const
 
                 // m.insert(names[Section], data(id, static_cast(Section)));
                 m.insert(names[Type], data(id, static_cast(Type)));
+                m.insert(names[TypeString], data(id, static_cast(TypeString)));
                 m.insert(names[Body], data(id, static_cast(Body)));
                 m.insert(names[FormattedBody], data(id, static_cast(FormattedBody)));
                 m.insert(names[UserId], data(id, static_cast(UserId)));
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 52ab28cf..44cf79f4 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -130,6 +130,7 @@ public:
         {
                 Section,
                 Type,
+                TypeString,
                 Body,
                 FormattedBody,
                 UserId,
-- 
cgit 1.5.1


From 4cd260bfcfcbf88a6efb8bf5a1abf3d37fb06463 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 31 Jan 2020 06:12:02 +0100
Subject: Optimize includes a bit

---
 CMakeLists.txt               |   1 +
 src/ChatPage.cpp             |   4 +-
 src/CommunitiesList.cpp      |  11 +++--
 src/CommunitiesList.h        |   5 ++
 src/CommunitiesListItem.cpp  |   3 ++
 src/CommunitiesListItem.h    |   7 +--
 src/LoginPage.cpp            |   1 +
 src/MainWindow.cpp           |   3 +-
 src/RegisterPage.cpp         |   1 +
 src/RoomInfoListItem.cpp     |   6 +--
 src/RoomList.cpp             |   6 +--
 src/SideBarActions.cpp       |   8 ++--
 src/SideBarActions.h         |   2 +-
 src/Splitter.cpp             |  22 +++++++--
 src/Splitter.h               |  16 ++++++-
 src/TextInputWidget.cpp      |   5 +-
 src/TextInputWidget.h        |   1 -
 src/TopRoomBar.cpp           |  26 +++++++++-
 src/TopRoomBar.h             |  35 +++++---------
 src/TrayIcon.cpp             |   2 +
 src/TrayIcon.h               |   5 +-
 src/UserInfoWidget.cpp       |   5 +-
 src/UserSettingsPage.cpp     |   1 +
 src/Utils.cpp                |  32 ++++++++-----
 src/Utils.h                  |  33 +------------
 src/WelcomePage.cpp          |   1 +
 src/dialogs/MemberList.cpp   |   3 +-
 src/dialogs/UserProfile.cpp  |   3 +-
 src/emoji/ItemDelegate.cpp   |   1 -
 src/emoji/Panel.cpp          |   1 +
 src/popups/UserMentions.h    |   7 +--
 src/timeline/TimelineModel.h |   8 ++--
 src/ui/DropShadow.cpp        | 110 +++++++++++++++++++++++++++++++++++++++++++
 src/ui/DropShadow.h          | 100 ++-------------------------------------
 src/ui/FlatButton.cpp        |   2 +
 src/ui/FlatButton.h          |   2 -
 src/ui/FloatingButton.cpp    |   1 +
 src/ui/LoadingIndicator.cpp  |   5 +-
 src/ui/LoadingIndicator.h    |   6 +--
 src/ui/OverlayWidget.cpp     |   4 +-
 src/ui/OverlayWidget.h       |   4 +-
 41 files changed, 271 insertions(+), 228 deletions(-)
 create mode 100644 src/ui/DropShadow.cpp

(limited to 'src/timeline/TimelineModel.h')

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 96a6aec6..fe00d570 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -256,6 +256,7 @@ set(SRC_FILES
 	# UI components
 	src/ui/Avatar.cpp
 	src/ui/Badge.cpp
+	src/ui/DropShadow.cpp
 	src/ui/LoadingIndicator.cpp
 	src/ui/InfoMessage.cpp
 	src/ui/FlatButton.cpp
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 2191c6de..e54892a6 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -84,7 +84,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
         // SideBar
         sideBar_ = new QFrame(this);
         sideBar_->setObjectName("sideBar");
-        sideBar_->setMinimumWidth(utils::calculateSidebarSizes(QFont{}).normal);
+        sideBar_->setMinimumWidth(::splitter::calculateSidebarSizes(QFont{}).normal);
         sideBarLayout_ = new QVBoxLayout(sideBar_);
         sideBarLayout_->setSpacing(0);
         sideBarLayout_->setMargin(0);
@@ -1307,7 +1307,7 @@ ChatPage::timelineWidth()
 bool
 ChatPage::isSideBarExpanded()
 {
-        const auto sz = utils::calculateSidebarSizes(QFont{});
+        const auto sz = splitter::calculateSidebarSizes(QFont{});
         return sideBar_->size().width() > sz.normal;
 }
 
diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp
index 4ea99408..2d97594a 100644
--- a/src/CommunitiesList.cpp
+++ b/src/CommunitiesList.cpp
@@ -2,7 +2,9 @@
 #include "Cache.h"
 #include "Logging.h"
 #include "MatrixClient.h"
-#include "Utils.h"
+#include "Splitter.h"
+
+#include 
 
 #include 
 
@@ -20,7 +22,7 @@ CommunitiesList::CommunitiesList(QWidget *parent)
         topLayout_->setSpacing(0);
         topLayout_->setMargin(0);
 
-        const auto sideBarSizes = utils::calculateSidebarSizes(QFont{});
+        const auto sideBarSizes = splitter::calculateSidebarSizes(QFont{});
         setFixedWidth(sideBarSizes.groups);
 
         scrollArea_ = new QScrollArea(this);
@@ -185,7 +187,8 @@ void
 CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img)
 {
         if (!communityExists(community_id)) {
-                qWarning() << "Avatar update on nonexistent community" << community_id;
+                nhlog::ui()->warn("Avatar update on nonexistent community {}",
+                                  community_id.toStdString());
                 return;
         }
 
@@ -196,7 +199,7 @@ void
 CommunitiesList::highlightSelectedCommunity(const QString &community_id)
 {
         if (!communityExists(community_id)) {
-                qDebug() << "CommunitiesList: clicked unknown community";
+                nhlog::ui()->debug("CommunitiesList: clicked unknown community");
                 return;
         }
 
diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h
index 49eaeaf6..e8042666 100644
--- a/src/CommunitiesList.h
+++ b/src/CommunitiesList.h
@@ -8,6 +8,11 @@
 #include "CommunitiesListItem.h"
 #include "ui/Theme.h"
 
+namespace mtx::responses {
+struct GroupProfile;
+struct JoinedGroups;
+}
+
 class CommunitiesList : public QWidget
 {
         Q_OBJECT
diff --git a/src/CommunitiesListItem.cpp b/src/CommunitiesListItem.cpp
index 324482d3..274271e5 100644
--- a/src/CommunitiesListItem.cpp
+++ b/src/CommunitiesListItem.cpp
@@ -1,4 +1,7 @@
 #include "CommunitiesListItem.h"
+
+#include 
+
 #include "Utils.h"
 #include "ui/Painter.h"
 #include "ui/Ripple.h"
diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h
index d4d7e9c6..0cc5d60c 100644
--- a/src/CommunitiesListItem.h
+++ b/src/CommunitiesListItem.h
@@ -1,17 +1,14 @@
 #pragma once
 
-#include 
-#include 
-#include 
 #include 
 #include 
 
-#include 
-
 #include "Config.h"
 #include "ui/Theme.h"
 
 class RippleOverlay;
+class QPainter;
+class QMouseEvent;
 
 class CommunitiesListItem : public QWidget
 {
diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index 0e7a18d4..c244db28 100644
--- a/src/LoginPage.cpp
+++ b/src/LoginPage.cpp
@@ -15,6 +15,7 @@
  * along with this program.  If not, see .
  */
 
+#include 
 #include 
 
 #include 
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index a24266fa..d400ad8e 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -31,6 +31,7 @@
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "RegisterPage.h"
+#include "Splitter.h"
 #include "TrayIcon.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
@@ -191,7 +192,7 @@ MainWindow::resizeEvent(QResizeEvent *event)
 void
 MainWindow::adjustSideBars()
 {
-        const auto sz = utils::calculateSidebarSizes(QFont{});
+        const auto sz = splitter::calculateSidebarSizes(QFont{});
 
         const uint64_t timelineWidth     = chat_page_->timelineWidth();
         const uint64_t minAvailableWidth = sz.collapsePoint + sz.groups;
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index fdb0f43a..942fd1b8 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -15,6 +15,7 @@
  * along with this program.  If not, see .
  */
 
+#include 
 #include 
 #include 
 
diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp
index 926e1359..822a7a55 100644
--- a/src/RoomInfoListItem.cpp
+++ b/src/RoomInfoListItem.cpp
@@ -16,7 +16,6 @@
  */
 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -26,6 +25,7 @@
 #include "Cache.h"
 #include "Config.h"
 #include "RoomInfoListItem.h"
+#include "Splitter.h"
 #include "Utils.h"
 #include "ui/Menu.h"
 #include "ui/Ripple.h"
@@ -116,7 +116,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *)
         QPainterPath path;
         path.addRect(0, 0, width(), height());
 
-        const auto sidebarSizes = utils::calculateSidebarSizes(QFont{});
+        const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{});
 
         if (width() > sidebarSizes.small)
                 setToolTip("");
@@ -165,7 +165,7 @@ RoomInfoListItem::paintEvent(QPaintEvent *event)
         // Description line with the default font.
         int bottom_y = wm.maxHeight - wm.padding - metrics.ascent() / 2;
 
-        const auto sidebarSizes = utils::calculateSidebarSizes(QFont{});
+        const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{});
 
         if (width() > sidebarSizes.small) {
                 QFont headingFont;
diff --git a/src/RoomList.cpp b/src/RoomList.cpp
index 6434489e..b90c8fa4 100644
--- a/src/RoomList.cpp
+++ b/src/RoomList.cpp
@@ -17,18 +17,14 @@
 
 #include 
 
-#include 
-#include 
 #include 
+#include 
 #include 
 
-#include "Cache.h"
 #include "Logging.h"
 #include "MainWindow.h"
-#include "MatrixClient.h"
 #include "RoomInfoListItem.h"
 #include "RoomList.h"
-#include "UserSettingsPage.h"
 #include "Utils.h"
 #include "ui/OverlayModal.h"
 
diff --git a/src/SideBarActions.cpp b/src/SideBarActions.cpp
index 2f447cd8..4934ec05 100644
--- a/src/SideBarActions.cpp
+++ b/src/SideBarActions.cpp
@@ -1,15 +1,15 @@
-#include 
 #include 
+#include 
+#include 
 
 #include 
 
 #include "Config.h"
 #include "MainWindow.h"
 #include "SideBarActions.h"
-#include "Utils.h"
+#include "Splitter.h"
 #include "ui/FlatButton.h"
 #include "ui/Menu.h"
-#include "ui/OverlayModal.h"
 
 SideBarActions::SideBarActions(QWidget *parent)
   : QWidget{parent}
@@ -93,7 +93,7 @@ SideBarActions::resizeEvent(QResizeEvent *event)
 {
         Q_UNUSED(event);
 
-        const auto sidebarSizes = utils::calculateSidebarSizes(QFont{});
+        const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{});
 
         if (width() <= sidebarSizes.small) {
                 roomDirectory_->hide();
diff --git a/src/SideBarActions.h b/src/SideBarActions.h
index ce96cba8..662750b3 100644
--- a/src/SideBarActions.h
+++ b/src/SideBarActions.h
@@ -2,7 +2,6 @@
 
 #include 
 #include 
-#include 
 #include 
 
 namespace mtx {
@@ -13,6 +12,7 @@ struct CreateRoom;
 
 class Menu;
 class FlatButton;
+class QResizeEvent;
 
 class SideBarActions : public QWidget
 {
diff --git a/src/Splitter.cpp b/src/Splitter.cpp
index ddb1dc1c..32c67425 100644
--- a/src/Splitter.cpp
+++ b/src/Splitter.cpp
@@ -16,19 +16,20 @@
  */
 
 #include 
-#include 
 #include 
 #include 
 #include 
 
 #include "Config.h"
+#include "Logging.h"
 #include "Splitter.h"
+#include "Utils.h"
 
 constexpr auto MaxWidth = (1 << 24) - 1;
 
 Splitter::Splitter(QWidget *parent)
   : QSplitter(parent)
-  , sz_{utils::calculateSidebarSizes(QFont{})}
+  , sz_{splitter::calculateSidebarSizes(QFont{})}
 {
         connect(this, &QSplitter::splitterMoved, this, &Splitter::onSplitterMoved);
         setChildrenCollapsible(false);
@@ -80,7 +81,7 @@ Splitter::onSplitterMoved(int pos, int index)
         auto s = sizes();
 
         if (s.count() < 2) {
-                qWarning() << "Splitter needs at least two children";
+                nhlog::ui()->warn("Splitter needs at least two children");
                 return;
         }
 
@@ -165,3 +166,18 @@ Splitter::showFullRoomList()
         left->show();
         left->setMaximumWidth(MaxWidth);
 }
+
+splitter::SideBarSizes
+splitter::calculateSidebarSizes(const QFont &f)
+{
+        const auto height = static_cast(QFontMetrics{f}.lineSpacing());
+
+        SideBarSizes sz;
+        sz.small         = std::ceil(3.5 * height + height / 4.0);
+        sz.normal        = std::ceil(16 * height);
+        sz.groups        = std::ceil(3 * height);
+        sz.collapsePoint = 2 * sz.normal;
+
+        return sz;
+}
+
diff --git a/src/Splitter.h b/src/Splitter.h
index 14d6773e..36c9f4fb 100644
--- a/src/Splitter.h
+++ b/src/Splitter.h
@@ -17,9 +17,21 @@
 
 #pragma once
 
-#include "Utils.h"
 #include 
 
+namespace splitter {
+struct SideBarSizes
+{
+        int small;
+        int normal;
+        int groups;
+        int collapsePoint;
+};
+
+SideBarSizes
+calculateSidebarSizes(const QFont &f);
+}
+
 class Splitter : public QSplitter
 {
         Q_OBJECT
@@ -45,5 +57,5 @@ private:
         int leftMoveCount_  = 0;
         int rightMoveCount_ = 0;
 
-        utils::SideBarSizes sz_;
+        splitter::SideBarSizes sz_;
 };
diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index 502456bf..7be50ab5 100644
--- a/src/TextInputWidget.cpp
+++ b/src/TextInputWidget.cpp
@@ -16,12 +16,9 @@
  */
 
 #include 
-#include 
 #include 
 #include 
-#include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -31,7 +28,7 @@
 
 #include "Cache.h"
 #include "ChatPage.h"
-#include "Config.h"
+#include "Logging.h"
 #include "TextInputWidget.h"
 #include "Utils.h"
 #include "ui/FlatButton.h"
diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index 4bdb2509..a430aa5c 100644
--- a/src/TextInputWidget.h
+++ b/src/TextInputWidget.h
@@ -23,7 +23,6 @@
 #include 
 
 #include 
-#include 
 #include 
 #include 
 #include 
diff --git a/src/TopRoomBar.cpp b/src/TopRoomBar.cpp
index 712fe9aa..ffd57d50 100644
--- a/src/TopRoomBar.cpp
+++ b/src/TopRoomBar.cpp
@@ -15,8 +15,16 @@
  * along with this program.  If not, see .
  */
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 
 #include "Config.h"
 #include "MainWindow.h"
@@ -210,3 +218,19 @@ TopRoomBar::updateRoomTopic(QString topic)
         topicLabel_->setHtml(topic);
         update();
 }
+
+void
+TopRoomBar::mousePressEvent(QMouseEvent *)
+{
+        if (roomSettings_ != nullptr)
+                roomSettings_->trigger();
+}
+
+void
+TopRoomBar::paintEvent(QPaintEvent *)
+{
+        QStyleOption opt;
+        opt.init(this);
+        QPainter p(this);
+        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+}
diff --git a/src/TopRoomBar.h b/src/TopRoomBar.h
index 3243064e..5ab25f39 100644
--- a/src/TopRoomBar.h
+++ b/src/TopRoomBar.h
@@ -17,17 +17,9 @@
 
 #pragma once
 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
+#include 
+#include 
+#include 
 
 class Avatar;
 class FlatButton;
@@ -35,6 +27,12 @@ class Menu;
 class TextLabel;
 class OverlayModal;
 
+class QPainter;
+class QLabel;
+class QIcon;
+class QHBoxLayout;
+class QVBoxLayout;
+
 class TopRoomBar : public QWidget
 {
         Q_OBJECT
@@ -67,19 +65,8 @@ signals:
         void mentionsClicked(const QPoint &pos);
 
 protected:
-        void mousePressEvent(QMouseEvent *) override
-        {
-                if (roomSettings_ != nullptr)
-                        roomSettings_->trigger();
-        }
-
-        void paintEvent(QPaintEvent *) override
-        {
-                QStyleOption opt;
-                opt.init(this);
-                QPainter p(this);
-                style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-        }
+        void mousePressEvent(QMouseEvent *) override;
+        void paintEvent(QPaintEvent *) override;
 
 private:
         QHBoxLayout *topLayout_  = nullptr;
diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp
index 8f62e563..6ab011d1 100644
--- a/src/TrayIcon.cpp
+++ b/src/TrayIcon.cpp
@@ -15,9 +15,11 @@
  * along with this program.  If not, see .
  */
 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include "TrayIcon.h"
diff --git a/src/TrayIcon.h b/src/TrayIcon.h
index a3536cc3..6cb26b87 100644
--- a/src/TrayIcon.h
+++ b/src/TrayIcon.h
@@ -17,13 +17,14 @@
 
 #pragma once
 
-#include 
 #include 
 #include 
-#include 
 #include 
 #include 
 
+class QAction;
+class QPainter;
+
 class MsgCountComposedIcon : public QIconEngine
 {
 public:
diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp
index 7a910340..2e21d41f 100644
--- a/src/UserInfoWidget.cpp
+++ b/src/UserInfoWidget.cpp
@@ -16,14 +16,15 @@
  * along with this program.  If not, see .
  */
 
+#include 
 #include 
 
 #include 
 
 #include "Config.h"
 #include "MainWindow.h"
+#include "Splitter.h"
 #include "UserInfoWidget.h"
-#include "Utils.h"
 #include "ui/Avatar.h"
 #include "ui/FlatButton.h"
 #include "ui/OverlayModal.h"
@@ -108,7 +109,7 @@ UserInfoWidget::resizeEvent(QResizeEvent *event)
 {
         Q_UNUSED(event);
 
-        const auto sz = utils::calculateSidebarSizes(QFont{});
+        const auto sz = splitter::calculateSidebarSizes(QFont{});
 
         if (width() <= sz.small) {
                 topLayout_->setContentsMargins(0, 0, logoutButtonSize_, 0);
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index b73f80a1..a0f37c26 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -22,6 +22,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 00796a53..55eebbc9 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -22,6 +22,25 @@ using TimelineEvent = mtx::events::collections::TimelineEvents;
 
 QHash authorColors_;
 
+template
+static DescInfo
+createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id)
+{
+        const auto msg    = std::get(event);
+        const auto sender = QString::fromStdString(msg.sender);
+
+        const auto username = cache::displayName(room_id, sender);
+        const auto ts       = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
+
+        return DescInfo{
+          QString::fromStdString(msg.event_id),
+          sender,
+          utils::messageDescription(
+            username, QString::fromStdString(msg.content.body).trimmed(), sender == localUser),
+          utils::descriptiveTime(ts),
+          ts};
+}
+
 QString
 utils::localUser()
 {
@@ -633,16 +652,3 @@ utils::restoreCombobox(QComboBox *combo, const QString &value)
         }
 }
 
-utils::SideBarSizes
-utils::calculateSidebarSizes(const QFont &f)
-{
-        const auto height = static_cast(QFontMetrics{f}.lineSpacing());
-
-        SideBarSizes sz;
-        sz.small         = std::ceil(3.5 * height + height / 4.0);
-        sz.normal        = std::ceil(16 * height);
-        sz.groups        = std::ceil(3 * height);
-        sz.collapsePoint = 2 * sz.normal;
-
-        return sz;
-}
diff --git a/src/Utils.h b/src/Utils.h
index ea0df21f..a3854dd8 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -2,8 +2,6 @@
 
 #include 
 
-#include "RoomInfoListItem.h"
-
 #include 
 #include 
 #include 
@@ -12,6 +10,8 @@
 
 #include 
 
+struct DescInfo;
+
 namespace cache {
 // Forward declarations to prevent dependency on Cache.h, since this header is included often!
 QString
@@ -166,25 +166,6 @@ messageDescription(const QString &username = "",
         }
 }
 
-template
-DescInfo
-createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id)
-{
-        const auto msg    = std::get(event);
-        const auto sender = QString::fromStdString(msg.sender);
-
-        const auto username = cache::displayName(room_id, sender);
-        const auto ts       = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
-
-        return DescInfo{QString::fromStdString(msg.event_id),
-                        sender,
-                        messageDescription(username,
-                                              QString::fromStdString(msg.content.body).trimmed(),
-                                              sender == localUser),
-                        utils::descriptiveTime(ts),
-                        ts};
-}
-
 //! Scale down an image to fit to the given width & height limitations.
 QPixmap
 scaleDown(uint64_t maxWidth, uint64_t maxHeight, const QPixmap &source);
@@ -326,14 +307,4 @@ centerWidget(QWidget *widget, QWidget *parent);
 void
 restoreCombobox(QComboBox *combo, const QString &value);
 
-struct SideBarSizes
-{
-        int small;
-        int normal;
-        int groups;
-        int collapsePoint;
-};
-
-SideBarSizes
-calculateSidebarSizes(const QFont &f);
 }
diff --git a/src/WelcomePage.cpp b/src/WelcomePage.cpp
index 8c3f6487..e4b0e1c6 100644
--- a/src/WelcomePage.cpp
+++ b/src/WelcomePage.cpp
@@ -17,6 +17,7 @@
 
 #include 
 #include 
+#include 
 #include 
 
 #include "Config.h"
diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp
index dfb3d984..54e7bf96 100644
--- a/src/dialogs/MemberList.cpp
+++ b/src/dialogs/MemberList.cpp
@@ -13,6 +13,7 @@
 #include "Cache.h"
 #include "ChatPage.h"
 #include "Config.h"
+#include "Logging.h"
 #include "Utils.h"
 #include "ui/Avatar.h"
 
@@ -116,7 +117,7 @@ MemberList::MemberList(const QString &room_id, QWidget *parent)
         try {
                 addUsers(cache::getMembers(room_id_.toStdString()));
         } catch (const lmdb::error &e) {
-                qCritical() << e.what();
+                nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what());
         }
 
         auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
diff --git a/src/dialogs/UserProfile.cpp b/src/dialogs/UserProfile.cpp
index 755e8395..273ffd54 100644
--- a/src/dialogs/UserProfile.cpp
+++ b/src/dialogs/UserProfile.cpp
@@ -1,13 +1,12 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 
-#include "AvatarProvider.h"
 #include "Cache.h"
 #include "ChatPage.h"
+#include "Logging.h"
 #include "MatrixClient.h"
 #include "Utils.h"
 #include "dialogs/UserProfile.h"
diff --git a/src/emoji/ItemDelegate.cpp b/src/emoji/ItemDelegate.cpp
index 890e334a..afa01625 100644
--- a/src/emoji/ItemDelegate.cpp
+++ b/src/emoji/ItemDelegate.cpp
@@ -15,7 +15,6 @@
  * along with this program.  If not, see .
  */
 
-#include 
 #include 
 #include 
 
diff --git a/src/emoji/Panel.cpp b/src/emoji/Panel.cpp
index 49ff40c5..e3b966b4 100644
--- a/src/emoji/Panel.cpp
+++ b/src/emoji/Panel.cpp
@@ -15,6 +15,7 @@
  * along with this program.  If not, see .
  */
 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/popups/UserMentions.h b/src/popups/UserMentions.h
index d3877462..42bdcd60 100644
--- a/src/popups/UserMentions.h
+++ b/src/popups/UserMentions.h
@@ -2,19 +2,14 @@
 
 #include 
 
-#include 
-#include 
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
 #include 
-#include 
 
-#include "Logging.h"
 
 namespace popups {
 
@@ -47,4 +42,4 @@ private:
         QScrollArea *all_scroll_area_;
         QWidget *all_scroll_widget_;
 };
-}
\ No newline at end of file
+}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 44cf79f4..fb32f0fb 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -6,16 +6,18 @@
 #include 
 #include 
 
-#include 
-#include 
 #include 
 
 #include "CacheCryptoStructs.h"
-#include "Logging.h"
 
 namespace mtx::http {
 using RequestErr = const std::optional &;
 }
+namespace mtx::responses {
+struct Timeline;
+struct Messages;
+struct ClaimKeys;
+}
 
 namespace qml_mtx_events {
 Q_NAMESPACE
diff --git a/src/ui/DropShadow.cpp b/src/ui/DropShadow.cpp
new file mode 100644
index 00000000..93baa02d
--- /dev/null
+++ b/src/ui/DropShadow.cpp
@@ -0,0 +1,110 @@
+#include "DropShadow.h"
+
+#include 
+#include 
+
+
+        void DropShadow::draw(QPainter &painter,
+                         qint16 margin,
+                         qreal radius,
+                         QColor start,
+                         QColor end,
+                         qreal startPosition,
+                         qreal endPosition0,
+                         qreal endPosition1,
+                         qreal width,
+                         qreal height)
+        {
+                painter.setPen(Qt::NoPen);
+
+                QLinearGradient gradient;
+                gradient.setColorAt(startPosition, start);
+                gradient.setColorAt(endPosition0, end);
+
+                // Right
+                QPointF right0(width - margin, height / 2);
+                QPointF right1(width, height / 2);
+                gradient.setStart(right0);
+                gradient.setFinalStop(right1);
+                painter.setBrush(QBrush(gradient));
+                // Deprecated in 5.13: painter.drawRoundRect(
+                //  QRectF(QPointF(width - margin * radius, margin), QPointF(width, height -
+                //  margin)), 0.0, 0.0);
+                painter.drawRoundedRect(
+                  QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)),
+                  0.0,
+                  0.0);
+
+                // Left
+                QPointF left0(margin, height / 2);
+                QPointF left1(0, height / 2);
+                gradient.setStart(left0);
+                gradient.setFinalStop(left1);
+                painter.setBrush(QBrush(gradient));
+                painter.drawRoundedRect(
+                  QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0);
+
+                // Top
+                QPointF top0(width / 2, margin);
+                QPointF top1(width / 2, 0);
+                gradient.setStart(top0);
+                gradient.setFinalStop(top1);
+                painter.setBrush(QBrush(gradient));
+                painter.drawRoundedRect(
+                  QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0);
+
+                // Bottom
+                QPointF bottom0(width / 2, height - margin);
+                QPointF bottom1(width / 2, height);
+                gradient.setStart(bottom0);
+                gradient.setFinalStop(bottom1);
+                painter.setBrush(QBrush(gradient));
+                painter.drawRoundedRect(
+                  QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)),
+                  0.0,
+                  0.0);
+
+                // BottomRight
+                QPointF bottomright0(width - margin, height - margin);
+                QPointF bottomright1(width, height);
+                gradient.setStart(bottomright0);
+                gradient.setFinalStop(bottomright1);
+                gradient.setColorAt(endPosition1, end);
+                painter.setBrush(QBrush(gradient));
+                painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0);
+
+                // BottomLeft
+                QPointF bottomleft0(margin, height - margin);
+                QPointF bottomleft1(0, height);
+                gradient.setStart(bottomleft0);
+                gradient.setFinalStop(bottomleft1);
+                gradient.setColorAt(endPosition1, end);
+                painter.setBrush(QBrush(gradient));
+                painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0);
+
+                // TopLeft
+                QPointF topleft0(margin, margin);
+                QPointF topleft1(0, 0);
+                gradient.setStart(topleft0);
+                gradient.setFinalStop(topleft1);
+                gradient.setColorAt(endPosition1, end);
+                painter.setBrush(QBrush(gradient));
+                painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0);
+
+                // TopRight
+                QPointF topright0(width - margin, margin);
+                QPointF topright1(width, 0);
+                gradient.setStart(topright0);
+                gradient.setFinalStop(topright1);
+                gradient.setColorAt(endPosition1, end);
+                painter.setBrush(QBrush(gradient));
+                painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0);
+
+                // Widget
+                painter.setBrush(QBrush("#FFFFFF"));
+                painter.setRenderHint(QPainter::Antialiasing);
+                painter.drawRoundedRect(
+                  QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)),
+                  radius,
+                  radius);
+        }
diff --git a/src/ui/DropShadow.h b/src/ui/DropShadow.h
index 60fc697b..6997e1a0 100644
--- a/src/ui/DropShadow.h
+++ b/src/ui/DropShadow.h
@@ -1,8 +1,8 @@
 #pragma once
 
 #include 
-#include 
-#include 
+
+class QPainter;
 
 class DropShadow
 {
@@ -16,99 +16,5 @@ public:
                          qreal endPosition0,
                          qreal endPosition1,
                          qreal width,
-                         qreal height)
-        {
-                painter.setPen(Qt::NoPen);
-
-                QLinearGradient gradient;
-                gradient.setColorAt(startPosition, start);
-                gradient.setColorAt(endPosition0, end);
-
-                // Right
-                QPointF right0(width - margin, height / 2);
-                QPointF right1(width, height / 2);
-                gradient.setStart(right0);
-                gradient.setFinalStop(right1);
-                painter.setBrush(QBrush(gradient));
-                // Deprecated in 5.13: painter.drawRoundRect(
-                //  QRectF(QPointF(width - margin * radius, margin), QPointF(width, height -
-                //  margin)), 0.0, 0.0);
-                painter.drawRoundedRect(
-                  QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)),
-                  0.0,
-                  0.0);
-
-                // Left
-                QPointF left0(margin, height / 2);
-                QPointF left1(0, height / 2);
-                gradient.setStart(left0);
-                gradient.setFinalStop(left1);
-                painter.setBrush(QBrush(gradient));
-                painter.drawRoundedRect(
-                  QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0);
-
-                // Top
-                QPointF top0(width / 2, margin);
-                QPointF top1(width / 2, 0);
-                gradient.setStart(top0);
-                gradient.setFinalStop(top1);
-                painter.setBrush(QBrush(gradient));
-                painter.drawRoundedRect(
-                  QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0);
-
-                // Bottom
-                QPointF bottom0(width / 2, height - margin);
-                QPointF bottom1(width / 2, height);
-                gradient.setStart(bottom0);
-                gradient.setFinalStop(bottom1);
-                painter.setBrush(QBrush(gradient));
-                painter.drawRoundedRect(
-                  QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)),
-                  0.0,
-                  0.0);
-
-                // BottomRight
-                QPointF bottomright0(width - margin, height - margin);
-                QPointF bottomright1(width, height);
-                gradient.setStart(bottomright0);
-                gradient.setFinalStop(bottomright1);
-                gradient.setColorAt(endPosition1, end);
-                painter.setBrush(QBrush(gradient));
-                painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0);
-
-                // BottomLeft
-                QPointF bottomleft0(margin, height - margin);
-                QPointF bottomleft1(0, height);
-                gradient.setStart(bottomleft0);
-                gradient.setFinalStop(bottomleft1);
-                gradient.setColorAt(endPosition1, end);
-                painter.setBrush(QBrush(gradient));
-                painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0);
-
-                // TopLeft
-                QPointF topleft0(margin, margin);
-                QPointF topleft1(0, 0);
-                gradient.setStart(topleft0);
-                gradient.setFinalStop(topleft1);
-                gradient.setColorAt(endPosition1, end);
-                painter.setBrush(QBrush(gradient));
-                painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0);
-
-                // TopRight
-                QPointF topright0(width - margin, margin);
-                QPointF topright1(width, 0);
-                gradient.setStart(topright0);
-                gradient.setFinalStop(topright1);
-                gradient.setColorAt(endPosition1, end);
-                painter.setBrush(QBrush(gradient));
-                painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0);
-
-                // Widget
-                painter.setBrush(QBrush("#FFFFFF"));
-                painter.setRenderHint(QPainter::Antialiasing);
-                painter.drawRoundedRect(
-                  QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)),
-                  radius,
-                  radius);
-        }
+                         qreal height);
 };
diff --git a/src/ui/FlatButton.cpp b/src/ui/FlatButton.cpp
index a828f582..6660c58d 100644
--- a/src/ui/FlatButton.cpp
+++ b/src/ui/FlatButton.cpp
@@ -2,6 +2,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/ui/FlatButton.h b/src/ui/FlatButton.h
index 9c2bf425..d29903c2 100644
--- a/src/ui/FlatButton.h
+++ b/src/ui/FlatButton.h
@@ -1,7 +1,5 @@
 #pragma once
 
-#include 
-#include 
 #include 
 #include 
 
diff --git a/src/ui/FloatingButton.cpp b/src/ui/FloatingButton.cpp
index 74dcd482..f3a09ccd 100644
--- a/src/ui/FloatingButton.cpp
+++ b/src/ui/FloatingButton.cpp
@@ -1,3 +1,4 @@
+#include 
 #include 
 
 #include "FloatingButton.h"
diff --git a/src/ui/LoadingIndicator.cpp b/src/ui/LoadingIndicator.cpp
index c8337089..d2b1240d 100644
--- a/src/ui/LoadingIndicator.cpp
+++ b/src/ui/LoadingIndicator.cpp
@@ -1,7 +1,8 @@
 #include "LoadingIndicator.h"
 
-#include 
-#include 
+#include 
+#include 
+#include 
 
 LoadingIndicator::LoadingIndicator(QWidget *parent)
   : QWidget(parent)
diff --git a/src/ui/LoadingIndicator.h b/src/ui/LoadingIndicator.h
index e8de0aec..1585098e 100644
--- a/src/ui/LoadingIndicator.h
+++ b/src/ui/LoadingIndicator.h
@@ -1,11 +1,11 @@
 #pragma once
 
 #include 
-#include 
-#include 
-#include 
 #include 
 
+class QPainter;
+class QTimer;
+class QPaintEvent;
 class LoadingIndicator : public QWidget
 {
         Q_OBJECT
diff --git a/src/ui/OverlayWidget.cpp b/src/ui/OverlayWidget.cpp
index ccac0116..a32d86b6 100644
--- a/src/ui/OverlayWidget.cpp
+++ b/src/ui/OverlayWidget.cpp
@@ -1,5 +1,7 @@
 #include "OverlayWidget.h"
-#include 
+
+#include 
+#include 
 
 OverlayWidget::OverlayWidget(QWidget *parent)
   : QWidget(parent)
diff --git a/src/ui/OverlayWidget.h b/src/ui/OverlayWidget.h
index 6662479d..ed3ef52d 100644
--- a/src/ui/OverlayWidget.h
+++ b/src/ui/OverlayWidget.h
@@ -1,10 +1,10 @@
 #pragma once
 
 #include 
-#include 
-#include 
 #include 
 
+class QPainter;
+
 class OverlayWidget : public QWidget
 {
         Q_OBJECT
-- 
cgit 1.5.1


From 7ccc120f6345bff8dad4abb349669973746aa8da Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Tue, 4 Feb 2020 04:58:43 +0100
Subject: modernize: use nullptr

---
 src/ChatPage.h                     | 2 +-
 src/LoginPage.cpp                  | 2 +-
 src/LoginPage.h                    | 2 +-
 src/MainWindow.h                   | 2 +-
 src/RegisterPage.cpp               | 8 ++++----
 src/RegisterPage.h                 | 2 +-
 src/RoomInfoListItem.h             | 2 +-
 src/RoomList.h                     | 2 +-
 src/TextInputWidget.h              | 2 +-
 src/TopRoomBar.h                   | 2 +-
 src/UserInfoWidget.h               | 2 +-
 src/UserSettingsPage.h             | 2 +-
 src/WelcomePage.h                  | 2 +-
 src/dialogs/ImageOverlay.cpp       | 2 +-
 src/popups/ReplyPopup.cpp          | 6 +++---
 src/popups/SuggestionsPopup.h      | 2 +-
 src/timeline/TimelineModel.h       | 2 +-
 src/timeline/TimelineViewManager.h | 2 +-
 src/ui/Avatar.h                    | 2 +-
 src/ui/Badge.h                     | 6 +++---
 src/ui/FlatButton.h                | 6 +++---
 src/ui/LoadingIndicator.h          | 2 +-
 src/ui/RaisedButton.h              | 4 ++--
 src/ui/Ripple.cpp                  | 2 +-
 src/ui/Ripple.h                    | 4 ++--
 src/ui/RippleOverlay.h             | 2 +-
 src/ui/TextField.cpp               | 8 ++++----
 src/ui/TextField.h                 | 2 +-
 src/ui/Theme.h                     | 2 +-
 29 files changed, 43 insertions(+), 43 deletions(-)

(limited to 'src/timeline/TimelineModel.h')

diff --git a/src/ChatPage.h b/src/ChatPage.h
index 9bc5fb73..8e2e9192 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -65,7 +65,7 @@ class ChatPage : public QWidget
         Q_OBJECT
 
 public:
-        ChatPage(QSharedPointer userSettings, QWidget *parent = 0);
+        ChatPage(QSharedPointer userSettings, QWidget *parent = nullptr);
 
         // Initialize all the components of the UI.
         void bootstrap(QString userid, QString homeserver, QString token);
diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index c06137c9..c3919b93 100644
--- a/src/LoginPage.cpp
+++ b/src/LoginPage.cpp
@@ -111,7 +111,7 @@ LoginPage::LoginPage(QWidget *parent)
 
         form_layout_->addLayout(matrixidLayout_);
         form_layout_->addWidget(password_input_);
-        form_layout_->addWidget(deviceName_, Qt::AlignHCenter, 0);
+        form_layout_->addWidget(deviceName_, Qt::AlignHCenter, nullptr);
         form_layout_->addLayout(serverLayout_);
 
         button_layout_ = new QHBoxLayout();
diff --git a/src/LoginPage.h b/src/LoginPage.h
index 99c249b1..4b84abfc 100644
--- a/src/LoginPage.h
+++ b/src/LoginPage.h
@@ -38,7 +38,7 @@ class LoginPage : public QWidget
         Q_OBJECT
 
 public:
-        LoginPage(QWidget *parent = 0);
+        LoginPage(QWidget *parent = nullptr);
 
         void reset();
 
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 1aadbf4d..59f29d50 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -62,7 +62,7 @@ class MainWindow : public QMainWindow
         Q_OBJECT
 
 public:
-        explicit MainWindow(QWidget *parent = 0);
+        explicit MainWindow(QWidget *parent = nullptr);
 
         static MainWindow *instance() { return instance_; };
         void saveCurrentWindowSize();
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index 76721036..2688e9a9 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -90,10 +90,10 @@ RegisterPage::RegisterPage(QWidget *parent)
         server_input_ = new TextField();
         server_input_->setLabel(tr("Home Server"));
 
-        form_layout_->addWidget(username_input_, Qt::AlignHCenter, 0);
-        form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0);
-        form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter, 0);
-        form_layout_->addWidget(server_input_, Qt::AlignHCenter, 0);
+        form_layout_->addWidget(username_input_, Qt::AlignHCenter, nullptr);
+        form_layout_->addWidget(password_input_, Qt::AlignHCenter, nullptr);
+        form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter, nullptr);
+        form_layout_->addWidget(server_input_, Qt::AlignHCenter, nullptr);
 
         button_layout_ = new QHBoxLayout();
         button_layout_->setSpacing(0);
diff --git a/src/RegisterPage.h b/src/RegisterPage.h
index b05cf150..96a5b3ca 100644
--- a/src/RegisterPage.h
+++ b/src/RegisterPage.h
@@ -30,7 +30,7 @@ class RegisterPage : public QWidget
         Q_OBJECT
 
 public:
-        RegisterPage(QWidget *parent = 0);
+        RegisterPage(QWidget *parent = nullptr);
 
 protected:
         void paintEvent(QPaintEvent *event) override;
diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h
index 16553c73..5cb9e83c 100644
--- a/src/RoomInfoListItem.h
+++ b/src/RoomInfoListItem.h
@@ -65,7 +65,7 @@ class RoomInfoListItem : public QWidget
         Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor)
 
 public:
-        RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent = 0);
+        RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent = nullptr);
 
         void updateUnreadMessageCount(int count, int highlightedCount);
         void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); };
diff --git a/src/RoomList.h b/src/RoomList.h
index 51a24043..fef552c6 100644
--- a/src/RoomList.h
+++ b/src/RoomList.h
@@ -35,7 +35,7 @@ class RoomList : public QWidget
         Q_OBJECT
 
 public:
-        explicit RoomList(QWidget *parent = 0);
+        explicit RoomList(QWidget *parent = nullptr);
 
         void initialize(const QMap &info);
         void sync(const std::map &info);
diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index 3674ee0d..ac79be8b 100644
--- a/src/TextInputWidget.h
+++ b/src/TextInputWidget.h
@@ -142,7 +142,7 @@ class TextInputWidget : public QWidget
         Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor)
 
 public:
-        TextInputWidget(QWidget *parent = 0);
+        TextInputWidget(QWidget *parent = nullptr);
 
         void stopTyping();
 
diff --git a/src/TopRoomBar.h b/src/TopRoomBar.h
index 5ab25f39..63ce847e 100644
--- a/src/TopRoomBar.h
+++ b/src/TopRoomBar.h
@@ -40,7 +40,7 @@ class TopRoomBar : public QWidget
         Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor)
 
 public:
-        TopRoomBar(QWidget *parent = 0);
+        TopRoomBar(QWidget *parent = nullptr);
 
         void updateRoomAvatar(const QString &avatar_image);
         void updateRoomAvatar(const QIcon &icon);
diff --git a/src/UserInfoWidget.h b/src/UserInfoWidget.h
index 263dd0c2..e1a925a4 100644
--- a/src/UserInfoWidget.h
+++ b/src/UserInfoWidget.h
@@ -31,7 +31,7 @@ class UserInfoWidget : public QWidget
         Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor)
 
 public:
-        UserInfoWidget(QWidget *parent = 0);
+        UserInfoWidget(QWidget *parent = nullptr);
 
         void setDisplayName(const QString &name);
         void setUserId(const QString &userid);
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 8658e80e..299905ab 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -147,7 +147,7 @@ class UserSettingsPage : public QWidget
         Q_OBJECT
 
 public:
-        UserSettingsPage(QSharedPointer settings, QWidget *parent = 0);
+        UserSettingsPage(QSharedPointer settings, QWidget *parent = nullptr);
 
 protected:
         void showEvent(QShowEvent *event) override;
diff --git a/src/WelcomePage.h b/src/WelcomePage.h
index 480dc702..ae660215 100644
--- a/src/WelcomePage.h
+++ b/src/WelcomePage.h
@@ -7,7 +7,7 @@ class WelcomePage : public QWidget
         Q_OBJECT
 
 public:
-        explicit WelcomePage(QWidget *parent = 0);
+        explicit WelcomePage(QWidget *parent = nullptr);
 
 protected:
         void paintEvent(QPaintEvent *) override;
diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp
index cbdd351c..a98c39c2 100644
--- a/src/dialogs/ImageOverlay.cpp
+++ b/src/dialogs/ImageOverlay.cpp
@@ -32,7 +32,7 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent)
   , originalImage_{image}
 {
         setMouseTracking(true);
-        setParent(0);
+        setParent(nullptr);
 
         setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
 
diff --git a/src/popups/ReplyPopup.cpp b/src/popups/ReplyPopup.cpp
index 42a5a6d3..5058c039 100644
--- a/src/popups/ReplyPopup.cpp
+++ b/src/popups/ReplyPopup.cpp
@@ -13,9 +13,9 @@
 
 ReplyPopup::ReplyPopup(QWidget *parent)
   : QWidget(parent)
-  , userItem_{0}
-  , msgLabel_{0}
-  , eventLabel_{0}
+  , userItem_{nullptr}
+  , msgLabel_{nullptr}
+  , eventLabel_{nullptr}
 {
         setAttribute(Qt::WA_ShowWithoutActivating, true);
         setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint);
diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h
index dcd054f8..f84870e7 100644
--- a/src/popups/SuggestionsPopup.h
+++ b/src/popups/SuggestionsPopup.h
@@ -59,7 +59,7 @@ private:
                 size_t posToRemove = layout_->count() - 1;
 
                 QLayoutItem *item;
-                while (startingPos <= posToRemove && (item = layout_->takeAt(posToRemove)) != 0) {
+                while (startingPos <= posToRemove && (item = layout_->takeAt(posToRemove)) != nullptr) {
                         delete item->widget();
                         delete item;
 
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index fb32f0fb..2d0e9627 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -126,7 +126,7 @@ class TimelineModel : public QAbstractListModel
                      typingUsersChanged)
 
 public:
-        explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0);
+        explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = nullptr);
 
         enum Roles
         {
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 5880a382..8552d6b3 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -29,7 +29,7 @@ class TimelineViewManager : public QObject
                      replyingEventChanged)
 
 public:
-        TimelineViewManager(QSharedPointer userSettings, QWidget *parent = 0);
+        TimelineViewManager(QSharedPointer userSettings, QWidget *parent = nullptr);
         QWidget *getWidget() const { return container; }
 
         void sync(const mtx::responses::Rooms &rooms);
diff --git a/src/ui/Avatar.h b/src/ui/Avatar.h
index a643c8c4..d6d0b5c7 100644
--- a/src/ui/Avatar.h
+++ b/src/ui/Avatar.h
@@ -15,7 +15,7 @@ class Avatar : public QWidget
         Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
 
 public:
-        explicit Avatar(QWidget *parent = 0, int size = ui::AvatarSize);
+        explicit Avatar(QWidget *parent = nullptr, int size = ui::AvatarSize);
 
         void setBackgroundColor(const QColor &color);
         void setIcon(const QIcon &icon);
diff --git a/src/ui/Badge.h b/src/ui/Badge.h
index fd73ad30..748b56fd 100644
--- a/src/ui/Badge.h
+++ b/src/ui/Badge.h
@@ -16,9 +16,9 @@ class Badge : public OverlayWidget
         Q_PROPERTY(QPointF relativePosition WRITE setRelativePosition READ relativePosition)
 
 public:
-        explicit Badge(QWidget *parent = 0);
-        explicit Badge(const QIcon &icon, QWidget *parent = 0);
-        explicit Badge(const QString &text, QWidget *parent = 0);
+        explicit Badge(QWidget *parent = nullptr);
+        explicit Badge(const QIcon &icon, QWidget *parent = nullptr);
+        explicit Badge(const QString &text, QWidget *parent = nullptr);
 
         void setBackgroundColor(const QColor &color);
         void setTextColor(const QColor &color);
diff --git a/src/ui/FlatButton.h b/src/ui/FlatButton.h
index d29903c2..764df6e9 100644
--- a/src/ui/FlatButton.h
+++ b/src/ui/FlatButton.h
@@ -91,14 +91,14 @@ class FlatButton : public QPushButton
         Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize)
 
 public:
-        explicit FlatButton(QWidget *parent         = 0,
+        explicit FlatButton(QWidget *parent         = nullptr,
                             ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
         explicit FlatButton(const QString &text,
-                            QWidget *parent         = 0,
+                            QWidget *parent         = nullptr,
                             ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
         FlatButton(const QString &text,
                    ui::Role role,
-                   QWidget *parent         = 0,
+                   QWidget *parent         = nullptr,
                    ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
         ~FlatButton();
 
diff --git a/src/ui/LoadingIndicator.h b/src/ui/LoadingIndicator.h
index 1585098e..e72a8c48 100644
--- a/src/ui/LoadingIndicator.h
+++ b/src/ui/LoadingIndicator.h
@@ -12,7 +12,7 @@ class LoadingIndicator : public QWidget
         Q_PROPERTY(QColor color READ color WRITE setColor)
 
 public:
-        LoadingIndicator(QWidget *parent = 0);
+        LoadingIndicator(QWidget *parent = nullptr);
 
         void paintEvent(QPaintEvent *e);
 
diff --git a/src/ui/RaisedButton.h b/src/ui/RaisedButton.h
index edd5ee4a..74543262 100644
--- a/src/ui/RaisedButton.h
+++ b/src/ui/RaisedButton.h
@@ -11,8 +11,8 @@ class RaisedButton : public FlatButton
         Q_OBJECT
 
 public:
-        explicit RaisedButton(QWidget *parent = 0);
-        explicit RaisedButton(const QString &text, QWidget *parent = 0);
+        explicit RaisedButton(QWidget *parent = nullptr);
+        explicit RaisedButton(const QString &text, QWidget *parent = nullptr);
         ~RaisedButton();
 
 protected:
diff --git a/src/ui/Ripple.cpp b/src/ui/Ripple.cpp
index e22c4a62..ef8a62dd 100644
--- a/src/ui/Ripple.cpp
+++ b/src/ui/Ripple.cpp
@@ -3,7 +3,7 @@
 
 Ripple::Ripple(const QPoint ¢er, QObject *parent)
   : QParallelAnimationGroup(parent)
-  , overlay_(0)
+  , overlay_(nullptr)
   , radius_anim_(animate("radius"))
   , opacity_anim_(animate("opacity"))
   , radius_(0)
diff --git a/src/ui/Ripple.h b/src/ui/Ripple.h
index 9184f061..3701fb6c 100644
--- a/src/ui/Ripple.h
+++ b/src/ui/Ripple.h
@@ -16,8 +16,8 @@ class Ripple : public QParallelAnimationGroup
         Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity)
 
 public:
-        explicit Ripple(const QPoint ¢er, QObject *parent = 0);
-        Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent = 0);
+        explicit Ripple(const QPoint ¢er, QObject *parent = nullptr);
+        Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent = nullptr);
 
         inline void setOverlay(RippleOverlay *overlay);
 
diff --git a/src/ui/RippleOverlay.h b/src/ui/RippleOverlay.h
index 9ef91fbf..5d12aff7 100644
--- a/src/ui/RippleOverlay.h
+++ b/src/ui/RippleOverlay.h
@@ -11,7 +11,7 @@ class RippleOverlay : public OverlayWidget
         Q_OBJECT
 
 public:
-        explicit RippleOverlay(QWidget *parent = 0);
+        explicit RippleOverlay(QWidget *parent = nullptr);
 
         void addRipple(Ripple *ripple);
         void addRipple(const QPoint &position, qreal radius = 300);
diff --git a/src/ui/TextField.cpp b/src/ui/TextField.cpp
index 0ae2516d..6c1552a8 100644
--- a/src/ui/TextField.cpp
+++ b/src/ui/TextField.cpp
@@ -16,7 +16,7 @@ TextField::TextField(QWidget *parent)
         QPalette pal;
 
         state_machine_    = new TextFieldStateMachine(this);
-        label_            = 0;
+        label_            = nullptr;
         label_font_size_  = 15;
         show_label_       = false;
         background_color_ = pal.color(QPalette::Window);
@@ -230,9 +230,9 @@ TextFieldStateMachine::TextFieldStateMachine(TextField *parent)
         normal_state_  = new QState;
         focused_state_ = new QState;
 
-        label_       = 0;
-        offset_anim_ = 0;
-        color_anim_  = 0;
+        label_       = nullptr;
+        offset_anim_ = nullptr;
+        color_anim_  = nullptr;
         progress_    = 0.0;
 
         addState(normal_state_);
diff --git a/src/ui/TextField.h b/src/ui/TextField.h
index 1675a2e0..100fed31 100644
--- a/src/ui/TextField.h
+++ b/src/ui/TextField.h
@@ -22,7 +22,7 @@ class TextField : public QLineEdit
         Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
 
 public:
-        explicit TextField(QWidget *parent = 0);
+        explicit TextField(QWidget *parent = nullptr);
 
         void setInkColor(const QColor &color);
         void setBackgroundColor(const QColor &color);
diff --git a/src/ui/Theme.h b/src/ui/Theme.h
index d1d7e2a6..ecff02b5 100644
--- a/src/ui/Theme.h
+++ b/src/ui/Theme.h
@@ -78,7 +78,7 @@ class Theme : public QObject
 {
         Q_OBJECT
 public:
-        explicit Theme(QObject *parent = 0);
+        explicit Theme(QObject *parent = nullptr);
 
         QColor getColor(const QString &key) const;
 
-- 
cgit 1.5.1


From 9d90467e6a91cd647643c35a90cb18f9666bc5a3 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Tue, 4 Feb 2020 21:16:04 +0100
Subject: Select first room on startup

---
 src/RoomList.cpp              | 15 ++++++++++-----
 src/popups/SuggestionsPopup.h |  3 ++-
 src/timeline/TimelineModel.h  |  4 +++-
 3 files changed, 15 insertions(+), 7 deletions(-)

(limited to 'src/timeline/TimelineModel.h')

diff --git a/src/RoomList.cpp b/src/RoomList.cpp
index b90c8fa4..a80e0757 100644
--- a/src/RoomList.cpp
+++ b/src/RoomList.cpp
@@ -158,6 +158,8 @@ RoomList::initialize(const QMap &info)
         if (rooms_.empty())
                 return;
 
+        sortRoomsByLastMessage();
+
         auto room = firstRoom();
         if (room.second.isNull())
                 return;
@@ -479,13 +481,16 @@ RoomList::addInvitedRoom(const QString &room_id, const RoomInfo &info)
 std::pair>
 RoomList::firstRoom() const
 {
-        auto firstRoom = rooms_.begin();
+        for (int i = 0; i < contentsLayout_->count(); i++) {
+                auto item = qobject_cast(contentsLayout_->itemAt(i)->widget());
 
-        while (firstRoom->second.isNull() && firstRoom != rooms_.end())
-                firstRoom++;
+                if (item) {
+                        return std::pair>(
+                          item->roomId(), rooms_.at(item->roomId()));
+                }
+        }
 
-        return std::pair>(firstRoom->first,
-                                                                    firstRoom->second);
+        return {};
 }
 
 void
diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h
index f84870e7..63c44538 100644
--- a/src/popups/SuggestionsPopup.h
+++ b/src/popups/SuggestionsPopup.h
@@ -59,7 +59,8 @@ private:
                 size_t posToRemove = layout_->count() - 1;
 
                 QLayoutItem *item;
-                while (startingPos <= posToRemove && (item = layout_->takeAt(posToRemove)) != nullptr) {
+                while (startingPos <= posToRemove &&
+                       (item = layout_->takeAt(posToRemove)) != nullptr) {
                         delete item->widget();
                         delete item;
 
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 2d0e9627..642e9bbc 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -126,7 +126,9 @@ class TimelineModel : public QAbstractListModel
                      typingUsersChanged)
 
 public:
-        explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = nullptr);
+        explicit TimelineModel(TimelineViewManager *manager,
+                               QString room_id,
+                               QObject *parent = nullptr);
 
         enum Roles
         {
-- 
cgit 1.5.1


From 6bdc75d07319b3b2a846ee4a905288ec0d9371f6 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Thu, 20 Feb 2020 20:51:07 +0100
Subject: Reset user colors on theme change (in qml timeline)

---
 resources/qml/TimelineRow.qml               |  2 +-
 resources/qml/TimelineView.qml              |  4 +--
 resources/qml/delegates/MessageDelegate.qml |  2 +-
 resources/qml/delegates/Reply.qml           |  2 +-
 src/Cache.cpp                               | 51 -----------------------------
 src/Cache.h                                 |  9 -----
 src/Cache_p.h                               |  6 ----
 src/MainWindow.cpp                          |  3 --
 src/timeline/TimelineModel.cpp              | 15 ++-------
 src/timeline/TimelineModel.h                |  2 --
 src/timeline/TimelineViewManager.cpp        | 11 +++++++
 src/timeline/TimelineViewManager.h          |  2 ++
 12 files changed, 20 insertions(+), 89 deletions(-)

(limited to 'src/timeline/TimelineModel.h')

diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index a371f78b..fa427bfd 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -23,7 +23,7 @@ RowLayout {
 		Reply {
 			visible: model.replyTo
 			modelData: chat.model.getDump(model.replyTo)
-			userColor: chat.model.userColor(modelData.userId, colors.window)
+			userColor: timelineManager.userColor(modelData.userId, colors.window)
 		}
 
 		// actual message content
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 9730422d..46cf484b 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -218,7 +218,7 @@ Item {
 						Text { 
 							id: userName
 							text: chat.model.escapeEmoji(modelData.userName)
-							color: chat.model.userColor(modelData.userId, colors.window)
+							color: timelineManager.userColor(modelData.userId, colors.window)
 							textFormat: Text.RichText
 
 							MouseArea {
@@ -283,7 +283,7 @@ Item {
 						anchors.bottom: parent.bottom
 
 						modelData: chat.model ? chat.model.getDump(timelineManager.replyingEvent) : {}
-						userColor: chat.model ? chat.model.userColor(modelData.userId, colors.window) : colors.window
+						userColor: timelineManager.userColor(modelData.userId, colors.window)
 					}
 
 					ImageButton {
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index f043efec..ab633087 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -34,7 +34,7 @@ Item {
 			roleValue: MtxEvent.EmoteMessage
 			NoticeMessage {
 				formatted: chat.model.escapeEmoji(modelData.userName) + " " + model.data.formattedBody
-				color: chat.model.userColor(modelData.userId, colors.window)
+				color: timelineManager.userColor(modelData.userId, colors.window)
 			}
 		}
 		DelegateChoice {
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index a1ddc8f0..8bb9ed38 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -19,7 +19,7 @@ Rectangle {
 		anchors.bottom: replyContainer.bottom
 		width: 4
 
-		color: chat.model ? chat.model.userColor(reply.modelData.userId, colors.window) : colors.window
+		color: timelineManager.userColor(reply.modelData.userId, colors.window)
 	}
 
 	Column {
diff --git a/src/Cache.cpp b/src/Cache.cpp
index e73f7754..caa3f1ec 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -2186,7 +2186,6 @@ Cache::roomMembers(const std::string &room_id)
 
 QHash Cache::DisplayNames;
 QHash Cache::AvatarUrls;
-QHash Cache::UserColors;
 
 QString
 Cache::displayName(const QString &room_id, const QString &user_id)
@@ -2218,16 +2217,6 @@ Cache::avatarUrl(const QString &room_id, const QString &user_id)
         return QString();
 }
 
-QString
-Cache::userColor(const QString &user_id)
-{
-        if (UserColors.contains(user_id)) {
-                return UserColors[user_id];
-        }
-
-        return QString();
-}
-
 void
 Cache::insertDisplayName(const QString &room_id,
                          const QString &user_id,
@@ -2258,24 +2247,6 @@ Cache::removeAvatarUrl(const QString &room_id, const QString &user_id)
         AvatarUrls.remove(fmt);
 }
 
-void
-Cache::insertUserColor(const QString &user_id, const QString &color_name)
-{
-        UserColors.insert(user_id, color_name);
-}
-
-void
-Cache::removeUserColor(const QString &user_id)
-{
-        UserColors.remove(user_id);
-}
-
-void
-Cache::clearUserColors()
-{
-        UserColors.clear();
-}
-
 void
 to_json(json &j, const RoomInfo &info)
 {
@@ -2425,12 +2396,6 @@ avatarUrl(const QString &room_id, const QString &user_id)
         return instance_->avatarUrl(room_id, user_id);
 }
 
-QString
-userColor(const QString &user_id)
-{
-        return instance_->userColor(user_id);
-}
-
 void
 removeDisplayName(const QString &room_id, const QString &user_id)
 {
@@ -2441,11 +2406,6 @@ removeAvatarUrl(const QString &room_id, const QString &user_id)
 {
         instance_->removeAvatarUrl(room_id, user_id);
 }
-void
-removeUserColor(const QString &user_id)
-{
-        instance_->removeUserColor(user_id);
-}
 
 void
 insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name)
@@ -2457,17 +2417,6 @@ insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &a
 {
         instance_->insertAvatarUrl(room_id, user_id, avatar_url);
 }
-void
-insertUserColor(const QString &user_id, const QString &color_name)
-{
-        instance_->insertUserColor(user_id, color_name);
-}
-
-void
-clearUserColors()
-{
-        instance_->clearUserColors();
-}
 
 //! Load saved data for the display names & avatars.
 void
diff --git a/src/Cache.h b/src/Cache.h
index 69b3ef2d..bb042ea9 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -43,25 +43,16 @@ QString
 displayName(const QString &room_id, const QString &user_id);
 QString
 avatarUrl(const QString &room_id, const QString &user_id);
-QString
-userColor(const QString &user_id);
 
 void
 removeDisplayName(const QString &room_id, const QString &user_id);
 void
 removeAvatarUrl(const QString &room_id, const QString &user_id);
-void
-removeUserColor(const QString &user_id);
 
 void
 insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name);
 void
 insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url);
-void
-insertUserColor(const QString &user_id, const QString &color_name);
-
-void
-clearUserColors();
 
 //! Load saved data for the display names & avatars.
 void
diff --git a/src/Cache_p.h b/src/Cache_p.h
index eacb28a7..137408b9 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -51,11 +51,9 @@ public:
         static std::string displayName(const std::string &room_id, const std::string &user_id);
         static QString displayName(const QString &room_id, const QString &user_id);
         static QString avatarUrl(const QString &room_id, const QString &user_id);
-        static QString userColor(const QString &user_id);
 
         static void removeDisplayName(const QString &room_id, const QString &user_id);
         static void removeAvatarUrl(const QString &room_id, const QString &user_id);
-        static void removeUserColor(const QString &user_id);
 
         static void insertDisplayName(const QString &room_id,
                                       const QString &user_id,
@@ -63,9 +61,6 @@ public:
         static void insertAvatarUrl(const QString &room_id,
                                     const QString &user_id,
                                     const QString &avatar_url);
-        static void insertUserColor(const QString &user_id, const QString &color_name);
-
-        static void clearUserColors();
 
         //! Load saved data for the display names & avatars.
         void populateMembers();
@@ -484,7 +479,6 @@ private:
 
         static QHash DisplayNames;
         static QHash AvatarUrls;
-        static QHash UserColors;
 
         OlmSessionStorage session_storage;
 };
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 1b378ca9..17a04a41 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -115,9 +115,6 @@ MainWindow::MainWindow(QWidget *parent)
 
         connect(
           userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool)));
-        connect(userSettingsPage_, &UserSettingsPage::themeChanged, this, []() {
-                cache::clearUserColors();
-        });
         connect(
           userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged);
         connect(trayIcon_,
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 8de6bec6..cad39bc5 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -196,9 +196,6 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
                         if (idx >= 0)
                                 emit dataChanged(index(idx, 0), index(idx, 0));
                 });
-
-        connect(
-          ChatPage::instance(), &ChatPage::themeChanged, this, [this]() { userColors.clear(); });
 }
 
 QHash
@@ -650,15 +647,6 @@ TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs)
         prev_batch_token_ = QString::fromStdString(msgs.end);
 }
 
-QColor
-TimelineModel::userColor(QString id, QColor background)
-{
-        if (!userColors.contains(id))
-                userColors.insert(
-                  id, QColor(utils::generateContrastingHexColor(id, background.name())));
-        return userColors.value(id);
-}
-
 QString
 TimelineModel::displayName(QString id) const
 {
@@ -1446,7 +1434,8 @@ TimelineModel::formatTypingUsers(const std::vector &users, QColor bg)
 
         auto formatUser = [this, bg](const QString &user_id) -> QString {
                 auto uncoloredUsername = escapeEmoji(displayName(user_id).toHtmlEscaped());
-                QString prefix = QString("").arg(userColor(user_id, bg).name());
+                QString prefix =
+                  QString("").arg(manager_->userColor(user_id, bg).name());
 
                 // color only parts that don't have a font already specified
                 QString coloredUsername;
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 642e9bbc..f06de5d9 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -165,7 +165,6 @@ public:
         bool canFetchMore(const QModelIndex &) const override;
         void fetchMore(const QModelIndex &) override;
 
-        Q_INVOKABLE QColor userColor(QString id, QColor background);
         Q_INVOKABLE QString displayName(QString id) const;
         Q_INVOKABLE QString avatarUrl(QString id) const;
         Q_INVOKABLE QString formatDateSeparator(QDate date) const;
@@ -248,7 +247,6 @@ private:
         bool paginationInProgress = false;
         bool isProcessingPending  = false;
 
-        QHash userColors;
         QString currentId;
         std::vector typingUsers_;
 
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 7cd1432d..14f903a1 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -18,6 +18,8 @@ Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
 void
 TimelineViewManager::updateColorPalette()
 {
+        userColors.clear();
+
         if (settings->theme() == "light") {
                 QPalette lightActive(/*windowText*/ QColor("#333"),
                                      /*button*/ QColor("#333"),
@@ -53,6 +55,15 @@ TimelineViewManager::updateColorPalette()
         }
 }
 
+QColor
+TimelineViewManager::userColor(QString id, QColor background)
+{
+        if (!userColors.contains(id))
+                userColors.insert(
+                  id, QColor(utils::generateContrastingHexColor(id, background.name())));
+        return userColors.value(id);
+}
+
 TimelineViewManager::TimelineViewManager(QSharedPointer userSettings, QWidget *parent)
   : imgProvider(new MxcImageProvider())
   , colorImgProvider(new ColorImageProvider())
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 625999c9..338101c7 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -40,6 +40,7 @@ public:
         Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
         Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
         Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const;
+        Q_INVOKABLE QColor userColor(QString id, QColor background);
 
 signals:
         void clearRoomMessageCount(QString roomid);
@@ -118,4 +119,5 @@ private:
         QString replyingEvent_;
 
         QSharedPointer settings;
+        QHash userColors;
 };
-- 
cgit 1.5.1