From 8e611abe87dd78095a35d156874a5b70ca72eb3e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 30 Aug 2019 19:29:25 +0200 Subject: [PATCH 01/94] Replace timeline with empty qml view --- .gitignore | 5 +- CMakeLists.txt | 34 ++++++------ resources/qml/TimelineView.qml | 11 ++++ resources/res.qrc | 3 ++ src/ChatPage.cpp | 4 +- src/popups/UserMentions.cpp | 77 +++++++++++++++------------ src/timeline2/TimelineViewManager.cpp | 10 ++++ src/timeline2/TimelineViewManager.h | 72 +++++++++++++++++++++++++ 8 files changed, 163 insertions(+), 53 deletions(-) create mode 100644 resources/qml/TimelineView.qml create mode 100644 src/timeline2/TimelineViewManager.cpp create mode 100644 src/timeline2/TimelineViewManager.h diff --git a/.gitignore b/.gitignore index 43c9b7b4..23b84039 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ -build +/build* tags cscope* .clang_complete *wintoastlib* +/.ccls-cache +/.exrc +.gdb_history # GTAGS GTAGS diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d5aff7a..b9726dd8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,7 +69,7 @@ include(LMDB) # # Discover Qt dependencies. # -find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia REQUIRED) +find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 REQUIRED) find_package(Qt5DBus) if (APPLE) @@ -191,13 +191,14 @@ set(SRC_FILES src/emoji/Provider.cpp # Timeline - src/timeline/TimelineViewManager.cpp - src/timeline/TimelineItem.cpp - src/timeline/TimelineView.cpp - src/timeline/widgets/AudioItem.cpp - src/timeline/widgets/FileItem.cpp - src/timeline/widgets/ImageItem.cpp - src/timeline/widgets/VideoItem.cpp + src/timeline2/TimelineViewManager.cpp + #src/timeline/TimelineViewManager.cpp + #src/timeline/TimelineItem.cpp + #src/timeline/TimelineView.cpp + #src/timeline/widgets/AudioItem.cpp + #src/timeline/widgets/FileItem.cpp + #src/timeline/widgets/ImageItem.cpp + #src/timeline/widgets/VideoItem.cpp # UI components src/ui/Avatar.cpp @@ -333,13 +334,14 @@ qt5_wrap_cpp(MOC_HEADERS src/emoji/PickButton.h # Timeline - src/timeline/TimelineItem.h - src/timeline/TimelineView.h - src/timeline/TimelineViewManager.h - src/timeline/widgets/AudioItem.h - src/timeline/widgets/FileItem.h - src/timeline/widgets/ImageItem.h - src/timeline/widgets/VideoItem.h + src/timeline2/TimelineViewManager.h + #src/timeline/TimelineItem.h + #src/timeline/TimelineView.h + #src/timeline/TimelineViewManager.h + #src/timeline/widgets/AudioItem.h + #src/timeline/widgets/FileItem.h + #src/timeline/widgets/ImageItem.h + #src/timeline/widgets/VideoItem.h # UI components src/ui/Avatar.h @@ -405,6 +407,8 @@ set(COMMON_LIBS Qt5::Svg Qt5::Concurrent Qt5::Multimedia + Qt5::Qml + Qt5::QuickControls2 nlohmann_json::nlohmann_json) if(APPVEYOR_BUILD) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml new file mode 100644 index 00000000..d81e3ae1 --- /dev/null +++ b/resources/qml/TimelineView.qml @@ -0,0 +1,11 @@ +import QtQuick 2.1 + +Rectangle { + anchors.fill: parent + + Text { + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + } +} diff --git a/resources/res.qrc b/resources/res.qrc index ad27af5a..65770c8c 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -114,4 +114,7 @@ styles/nheko.qss styles/nheko-dark.qss + + qml/TimelineView.qml + diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 21ded4b3..594a41c2 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -44,7 +44,7 @@ #include "dialogs/ReadReceipts.h" #include "popups/UserMentions.h" -#include "timeline/TimelineViewManager.h" +#include "timeline2/TimelineViewManager.h" // TODO: Needs to be updated with an actual secret. static const std::string STORAGE_SECRET_KEY("secret"); @@ -113,7 +113,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) view_manager_ = new TimelineViewManager(this); contentLayout_->addWidget(top_bar_); - contentLayout_->addWidget(view_manager_); + contentLayout_->addWidget(view_manager_->getWidget()); connect(this, &ChatPage::removeTimelineEvent, diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp index 3480959a..3be5c462 100644 --- a/src/popups/UserMentions.cpp +++ b/src/popups/UserMentions.cpp @@ -7,7 +7,7 @@ #include "ChatPage.h" #include "Logging.h" #include "UserMentions.h" -#include "timeline/TimelineItem.h" +//#include "timeline/TimelineItem.h" using namespace popups; @@ -116,39 +116,46 @@ UserMentions::pushItem(const QString &event_id, const QString &room_id, const QString ¤t_room_id) { - setUpdatesEnabled(false); - - // Add to the 'all' section - TimelineItem *view_item = new TimelineItem( - mtx::events::MessageType::Text, user_id, body, true, room_id, all_scroll_widget_); - view_item->setEventId(event_id); - view_item->hide(); - - all_scroll_layout_->addWidget(view_item); - QTimer::singleShot(0, this, [view_item, this]() { - view_item->show(); - view_item->adjustSize(); - setUpdatesEnabled(true); - }); - - // if it matches the current room... add it to the current room as well. - if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) { - // Add to the 'local' section - TimelineItem *local_view_item = new TimelineItem(mtx::events::MessageType::Text, - user_id, - body, - true, - room_id, - local_scroll_widget_); - local_view_item->setEventId(event_id); - local_view_item->hide(); - local_scroll_layout_->addWidget(local_view_item); - - QTimer::singleShot(0, this, [local_view_item]() { - local_view_item->show(); - local_view_item->adjustSize(); - }); - } + (void)event_id; + (void)user_id; + (void)body; + (void)room_id; + (void)current_room_id; + // setUpdatesEnabled(false); + // + // // Add to the 'all' section + // TimelineItem *view_item = new TimelineItem( + // mtx::events::MessageType::Text, user_id, body, true, room_id, + // all_scroll_widget_); + // view_item->setEventId(event_id); + // view_item->hide(); + // + // all_scroll_layout_->addWidget(view_item); + // QTimer::singleShot(0, this, [view_item, this]() { + // view_item->show(); + // view_item->adjustSize(); + // setUpdatesEnabled(true); + // }); + // + // // if it matches the current room... add it to the current room as well. + // if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) { + // // Add to the 'local' section + // TimelineItem *local_view_item = new + // TimelineItem(mtx::events::MessageType::Text, + // user_id, + // body, + // true, + // room_id, + // local_scroll_widget_); + // local_view_item->setEventId(event_id); + // local_view_item->hide(); + // local_scroll_layout_->addWidget(local_view_item); + // + // QTimer::singleShot(0, this, [local_view_item]() { + // local_view_item->show(); + // local_view_item->adjustSize(); + // }); + // } } void @@ -158,4 +165,4 @@ UserMentions::paintEvent(QPaintEvent *) opt.init(this); QPainter p(this); style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} \ No newline at end of file +} diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp new file mode 100644 index 00000000..b932b6ac --- /dev/null +++ b/src/timeline2/TimelineViewManager.cpp @@ -0,0 +1,10 @@ +#include "TimelineViewManager.h" + +TimelineViewManager::TimelineViewManager(QWidget *parent) +{ + view = new QQuickView(); + container = QWidget::createWindowContainer(view, parent); + container->setMinimumSize(200, 200); + view->setSource(QUrl("qrc:///qml/TimelineView.qml")); + // view->rootContext()->setContextProperty(room); +} diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h new file mode 100644 index 00000000..23d30065 --- /dev/null +++ b/src/timeline2/TimelineViewManager.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include + +#include "Cache.h" +#include "Utils.h" + +// temporary for stubs +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" + +class TimelineViewManager : public QObject +{ + Q_OBJECT +public: + TimelineViewManager(QWidget *parent = 0); + QWidget *getWidget() const { return container; } + + void initialize(const mtx::responses::Rooms &rooms) {} + void addRoom(const QString &room_id) {} + + void sync(const mtx::responses::Rooms &rooms) {} + void clearAll() {} + +signals: + void clearRoomMessageCount(QString roomid); + void updateRoomsLastMessage(const QString &user, const DescInfo &info); + +public slots: + void updateReadReceipts(const QString &room_id, const std::vector &event_ids) {} + void removeTimelineEvent(const QString &room_id, const QString &event_id) {} + void initWithMessages(const std::map &msgs) {} + + void setHistoryView(const QString &room_id) {} + void queueTextMessage(const QString &msg) {} + void queueReplyMessage(const QString &reply, const RelatedInfo &related) {} + void queueEmoteMessage(const QString &msg) {} + void queueImageMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions) + {} + void queueFileMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) + {} + void queueAudioMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) + {} + void queueVideoMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) + {} + +private: + QQuickView *view; + QWidget *container; +}; + +#pragma GCC diagnostic pop From 8b5c7b2f2fb43b4f1884e683d60dd2553b9aa994 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 30 Aug 2019 23:20:53 +0200 Subject: [PATCH 02/94] Add placeholder timeline model --- CMakeLists.txt | 2 ++ resources/qml/TimelineView.qml | 11 +++++++ src/timeline2/TimelineModel.cpp | 47 +++++++++++++++++++++++++++ src/timeline2/TimelineModel.h | 42 ++++++++++++++++++++++++ src/timeline2/TimelineViewManager.cpp | 44 ++++++++++++++++++++++++- src/timeline2/TimelineViewManager.h | 15 ++++++--- 6 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 src/timeline2/TimelineModel.cpp create mode 100644 src/timeline2/TimelineModel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b9726dd8..8013fed9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,6 +192,7 @@ set(SRC_FILES # Timeline src/timeline2/TimelineViewManager.cpp + src/timeline2/TimelineModel.cpp #src/timeline/TimelineViewManager.cpp #src/timeline/TimelineItem.cpp #src/timeline/TimelineView.cpp @@ -335,6 +336,7 @@ qt5_wrap_cpp(MOC_HEADERS # Timeline src/timeline2/TimelineViewManager.h + src/timeline2/TimelineModel.h #src/timeline/TimelineItem.h #src/timeline/TimelineView.h #src/timeline/TimelineViewManager.h diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index d81e3ae1..a53c8a94 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -4,8 +4,19 @@ Rectangle { anchors.fill: parent Text { + visible: !timeline anchors.centerIn: parent text: qsTr("No room open") font.pointSize: 24 } + + ListView { + visible: timeline != undefined + anchors.fill: parent + + model: timeline + delegate: Text { + text: userId + } + } } diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp new file mode 100644 index 00000000..592064dd --- /dev/null +++ b/src/timeline2/TimelineModel.cpp @@ -0,0 +1,47 @@ +#include "TimelineModel.h" + +#include "Utils.h" + +QHash +TimelineModel::roleNames() const +{ + return { + {Type, "type"}, + {Body, "body"}, + {FormattedBody, "formattedBody"}, + {UserId, "userId"}, + {UserName, "userName"}, + {Timestamp, "timestamp"}, + }; +} +int +TimelineModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return (int)this->eventOrder.size(); +} + +QVariant +TimelineModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 && index.row() >= (int)eventOrder.size()) + return QVariant(); + + QString id = eventOrder[index.row()]; + + switch (role) { + case UserId: + return QVariant(QString("")); + default: + return QVariant(); + } +} + +QColor +TimelineModel::userColor(QString id, QColor background) +{ + if (!userColors.count(id)) + userColors.insert( + {id, QColor(utils::generateContrastingHexColor(id, background.name()))}); + return userColors.at(id); +} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h new file mode 100644 index 00000000..c281056d --- /dev/null +++ b/src/timeline2/TimelineModel.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include +#include + +#include + +class TimelineModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit TimelineModel(QObject *parent = 0) + : QAbstractListModel(parent) + {} + + enum Roles + { + Type, + Body, + FormattedBody, + UserId, + UserName, + Timestamp, + }; + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + Q_INVOKABLE QColor userColor(QString id, QColor background); + +private: + std::map events; + std::vector eventOrder; + + std::map userColors; +}; + diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index b932b6ac..711dfcad 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -1,10 +1,52 @@ #include "TimelineViewManager.h" +#include +#include + +#include "Logging.h" + TimelineViewManager::TimelineViewManager(QWidget *parent) { view = new QQuickView(); container = QWidget::createWindowContainer(view, parent); container->setMinimumSize(200, 200); view->setSource(QUrl("qrc:///qml/TimelineView.qml")); - // view->rootContext()->setContextProperty(room); +} + +void +TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) +{ + for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { + addRoom(QString::fromStdString(it->first)); + } + + sync(rooms); +} + +void +TimelineViewManager::addRoom(const QString &room_id) +{ + if (!models.contains(room_id)) + models.insert(room_id, QSharedPointer(new TimelineModel())); +} + +void +TimelineViewManager::setHistoryView(const QString &room_id) +{ + nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); + + auto room = models.find(room_id); + if (room != models.end()) { + view->rootContext()->setContextProperty("timeline", + QVariant::fromValue(room.value().data())); + nhlog::ui()->info("Activated room {}", room_id.toStdString()); + } +} + +void +TimelineViewManager::initWithMessages(const std::map &msgs) +{ + for (const auto &e : msgs) { + addRoom(e.first); + } } diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 23d30065..80948148 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -1,11 +1,13 @@ #pragma once #include +#include #include #include #include "Cache.h" +#include "TimelineModel.h" #include "Utils.h" // temporary for stubs @@ -19,11 +21,11 @@ public: TimelineViewManager(QWidget *parent = 0); QWidget *getWidget() const { return container; } - void initialize(const mtx::responses::Rooms &rooms) {} - void addRoom(const QString &room_id) {} + void initialize(const mtx::responses::Rooms &rooms); + void addRoom(const QString &room_id); void sync(const mtx::responses::Rooms &rooms) {} - void clearAll() {} + void clearAll() { models.clear(); } signals: void clearRoomMessageCount(QString roomid); @@ -32,9 +34,10 @@ signals: public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids) {} void removeTimelineEvent(const QString &room_id, const QString &event_id) {} - void initWithMessages(const std::map &msgs) {} + void initWithMessages(const std::map &msgs); + + void setHistoryView(const QString &room_id); - void setHistoryView(const QString &room_id) {} void queueTextMessage(const QString &msg) {} void queueReplyMessage(const QString &reply, const RelatedInfo &related) {} void queueEmoteMessage(const QString &msg) {} @@ -67,6 +70,8 @@ public slots: private: QQuickView *view; QWidget *container; + + QHash> models; }; #pragma GCC diagnostic pop From 47fbfd3f44154faf796c0be47dddfcba1b509a12 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 31 Aug 2019 22:43:31 +0200 Subject: [PATCH 03/94] Add items to timline --- resources/qml/TimelineView.qml | 19 +++++++--- src/timeline2/TimelineModel.cpp | 54 +++++++++++++++++++++++++-- src/timeline2/TimelineModel.h | 12 +++--- src/timeline2/TimelineViewManager.cpp | 10 +++-- src/timeline2/TimelineViewManager.h | 13 +++++++ 5 files changed, 89 insertions(+), 19 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index a53c8a94..fcf88167 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -4,19 +4,28 @@ Rectangle { anchors.fill: parent Text { - visible: !timeline + visible: !timelineManager.timeline anchors.centerIn: parent text: qsTr("No room open") font.pointSize: 24 } + Text { + visible: timelineManager.timeline != null + anchors.centerIn: parent + text: qsTr("room open") + font.pointSize: 24 + } ListView { - visible: timeline != undefined + visible: timelineManager.timeline != null anchors.fill: parent - model: timeline + id: chat + + model: timelineManager.timeline delegate: Text { - text: userId + height: contentHeight + text: model.userId } - } + } } diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 592064dd..b13a1e6a 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -1,7 +1,29 @@ #include "TimelineModel.h" +#include "Logging.h" #include "Utils.h" +namespace { +template +QString +eventId(const T &event) +{ + return QString::fromStdString(event.event_id); +} +template +QString +roomId(const T &event) +{ + return QString::fromStdString(event.room_id); +} +template +QString +senderId(const T &event) +{ + return QString::fromStdString(event.sender); +} +} + QHash TimelineModel::roleNames() const { @@ -18,12 +40,14 @@ int TimelineModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); + nhlog::ui()->info("current order size: {}", eventOrder.size()); return (int)this->eventOrder.size(); } QVariant TimelineModel::data(const QModelIndex &index, int role) const { + nhlog::ui()->info("data"); if (index.row() < 0 && index.row() >= (int)eventOrder.size()) return QVariant(); @@ -31,17 +55,39 @@ TimelineModel::data(const QModelIndex &index, int role) const switch (role) { case UserId: - return QVariant(QString("")); + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, events.value(id))); default: return QVariant(); } } +void +TimelineModel::addEvents(const mtx::responses::Timeline &events) +{ + nhlog::ui()->info("add {} events", events.events.size()); + std::vector ids; + for (const auto &e : events.events) { + QString id = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); + + this->events.insert(id, e); + ids.push_back(id); + nhlog::ui()->info("add event {}", id.toStdString()); + } + + beginInsertRows(QModelIndex(), + static_cast(this->events.size()), + static_cast(this->events.size() + ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); + endInsertRows(); +} + QColor TimelineModel::userColor(QString id, QColor background) { - if (!userColors.count(id)) + if (!userColors.contains(id)) userColors.insert( - {id, QColor(utils::generateContrastingHexColor(id, background.name()))}); - return userColors.at(id); + id, QColor(utils::generateContrastingHexColor(id, background.name()))); + return userColors.value(id); } diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index c281056d..2252621c 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -1,12 +1,10 @@ #pragma once -#include -#include - #include #include +#include -#include +#include class TimelineModel : public QAbstractListModel { @@ -33,10 +31,12 @@ public: Q_INVOKABLE QColor userColor(QString id, QColor background); + void addEvents(const mtx::responses::Timeline &events); + private: - std::map events; + QHash events; std::vector eventOrder; - std::map userColors; + QHash userColors; }; diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 711dfcad..0468fc2a 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -10,6 +10,7 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) view = new QQuickView(); container = QWidget::createWindowContainer(view, parent); container->setMinimumSize(200, 200); + view->rootContext()->setContextProperty("timelineManager", this); view->setSource(QUrl("qrc:///qml/TimelineView.qml")); } @@ -18,9 +19,8 @@ TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) { for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { addRoom(QString::fromStdString(it->first)); + models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline); } - - sync(rooms); } void @@ -37,8 +37,8 @@ TimelineViewManager::setHistoryView(const QString &room_id) auto room = models.find(room_id); if (room != models.end()) { - view->rootContext()->setContextProperty("timeline", - QVariant::fromValue(room.value().data())); + timeline_ = room.value().get(); + emit activeTimelineChanged(timeline_); nhlog::ui()->info("Activated room {}", room_id.toStdString()); } } @@ -48,5 +48,7 @@ TimelineViewManager::initWithMessages(const std::mapaddEvents(e.second); } } diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 80948148..7f760eac 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -7,6 +7,7 @@ #include #include "Cache.h" +#include "Logging.h" #include "TimelineModel.h" #include "Utils.h" @@ -17,6 +18,10 @@ class TimelineViewManager : public QObject { Q_OBJECT + + Q_PROPERTY( + TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) + public: TimelineViewManager(QWidget *parent = 0); QWidget *getWidget() const { return container; } @@ -27,9 +32,16 @@ public: void sync(const mtx::responses::Rooms &rooms) {} void clearAll() { models.clear(); } + Q_INVOKABLE TimelineModel *activeTimeline() const + { + nhlog::ui()->info("aaaa"); + return timeline_; + } + signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(const QString &user, const DescInfo &info); + void activeTimelineChanged(TimelineModel *timeline); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids) {} @@ -70,6 +82,7 @@ public slots: private: QQuickView *view; QWidget *container; + TimelineModel *timeline_ = nullptr; QHash> models; }; From 699fd7b38e3c03a78cf54b0864ec63b25818b695 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 31 Aug 2019 23:44:17 +0200 Subject: [PATCH 04/94] Implement loading of history, when timeline is displayed --- resources/qml/TimelineView.qml | 6 --- src/timeline2/TimelineModel.cpp | 65 +++++++++++++++++++++++++++ src/timeline2/TimelineModel.h | 21 +++++++-- src/timeline2/TimelineViewManager.cpp | 3 +- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index fcf88167..3d4c1147 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -9,12 +9,6 @@ Rectangle { text: qsTr("No room open") font.pointSize: 24 } - Text { - visible: timelineManager.timeline != null - anchors.centerIn: parent - text: qsTr("room open") - font.pointSize: 24 - } ListView { visible: timelineManager.timeline != null diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index b13a1e6a..10a5d3bf 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -24,6 +24,14 @@ senderId(const T &event) } } +TimelineModel::TimelineModel(QString room_id, QObject *parent) + : QAbstractListModel(parent) + , room_id_(room_id) +{ + connect( + this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); +} + QHash TimelineModel::roleNames() const { @@ -65,6 +73,11 @@ TimelineModel::data(const QModelIndex &index, int role) const void TimelineModel::addEvents(const mtx::responses::Timeline &events) { + if (isInitialSync) { + prev_batch_token_ = QString::fromStdString(events.prev_batch); + isInitialSync = false; + } + nhlog::ui()->info("add {} events", events.events.size()); std::vector ids; for (const auto &e : events.events) { @@ -83,6 +96,58 @@ TimelineModel::addEvents(const mtx::responses::Timeline &events) endInsertRows(); } +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); + return; + } + + emit oldMessagesRetrieved(std::move(res)); + }); +} + +void +TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) +{ + nhlog::ui()->info("add {} backwards events", msgs.chunk.size()); + std::vector ids; + for (const auto &e : msgs.chunk) { + QString id = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); + + this->events.insert(id, e); + ids.push_back(id); + nhlog::ui()->info("add event {}", id.toStdString()); + } + + beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); + endInsertRows(); + + prev_batch_token_ = QString::fromStdString(msgs.end); + + paginationInProgress = false; +} + QColor TimelineModel::userColor(QString id, QColor background) { diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 2252621c..a4224538 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -11,9 +11,7 @@ class TimelineModel : public QAbstractListModel Q_OBJECT public: - explicit TimelineModel(QObject *parent = 0) - : QAbstractListModel(parent) - {} + explicit TimelineModel(QString room_id, QObject *parent = 0); enum Roles { @@ -31,12 +29,29 @@ public: Q_INVOKABLE QColor userColor(QString id, QColor background); + void addEvents(const mtx::responses::Timeline &events); +public slots: + void fetchHistory(); + +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); + private: QHash events; std::vector eventOrder; + QString room_id_; + QString prev_batch_token_; + + bool isInitialSync = true; + bool paginationInProgress = false; + QHash userColors; }; diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 0468fc2a..32321fd2 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -27,7 +27,7 @@ void TimelineViewManager::addRoom(const QString &room_id) { if (!models.contains(room_id)) - models.insert(room_id, QSharedPointer(new TimelineModel())); + models.insert(room_id, QSharedPointer(new TimelineModel(room_id))); } void @@ -38,6 +38,7 @@ TimelineViewManager::setHistoryView(const QString &room_id) auto room = models.find(room_id); if (room != models.end()) { timeline_ = room.value().get(); + timeline_->fetchHistory(); emit activeTimelineChanged(timeline_); nhlog::ui()->info("Activated room {}", room_id.toStdString()); } From 2dd636456c22a85751e3484776c45efc9458b9ba Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 1 Sep 2019 14:17:33 +0200 Subject: [PATCH 05/94] Add basic sections and button placeholders to qml timeline --- .gitignore | 3 + resources/qml/TimelineView.qml | 90 ++++++++++++++++++++++++++-- src/timeline/.TimelineItem.cpp.swp | Bin 114688 -> 0 bytes src/timeline2/TimelineModel.cpp | 43 ++++++++++++- src/timeline2/TimelineModel.h | 1 + src/timeline2/TimelineViewManager.h | 1 - 6 files changed, 132 insertions(+), 6 deletions(-) delete mode 100644 src/timeline/.TimelineItem.cpp.swp diff --git a/.gitignore b/.gitignore index 23b84039..0f61a911 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,9 @@ ui_*.h *.qmlproject.user *.qmlproject.user.* +# Vim +*.swp + #####=== CMake ===##### CMakeCache.txt diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 3d4c1147..7ff51362 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -1,4 +1,6 @@ -import QtQuick 2.1 +import QtQuick 2.5 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.5 Rectangle { anchors.fill: parent @@ -17,9 +19,89 @@ Rectangle { id: chat model: timelineManager.timeline - delegate: Text { - height: contentHeight - text: model.userId + delegate: RowLayout { + width: chat.width + Text { + Layout.fillWidth: true + height: contentHeight + text: model.userName + } + + Button { + Layout.alignment: Qt.AlignRight + id: replyButton + flat: true + height: replyButtonImg.contentHeight + width: replyButtonImg.contentWidth + ToolTip.visible: hovered + ToolTip.text: qsTr("Reply") + Image { + id: replyButtonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "qrc:/icons/icons/ui/mail-reply.png" + } + } + Button { + Layout.alignment: Qt.AlignRight + id: optionsButton + flat: true + height: optionsButtonImg.contentHeight + width: optionsButtonImg.contentWidth + ToolTip.visible: hovered + ToolTip.text: qsTr("Options") + Image { + id: optionsButtonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "qrc:/icons/icons/ui/vertical-ellipsis.png" + } + + onClicked: contextMenu.open() + + Menu { + y: optionsButton.height + id: contextMenu + + MenuItem { + text: "Read receipts" + } + MenuItem { + text: "Mark as read" + } + MenuItem { + text: "View raw message" + } + MenuItem { + text: "Redact message" + } + } + } + + Text { + Layout.alignment: Qt.AlignRight + text: model.timestamp.toLocaleTimeString("HH:mm") + } + } + + section { + property: "section" + delegate: Column { + width: parent.width + Label { + anchors.horizontalCenter: parent.horizontalCenter + visible: section.includes(" ") + text: Qt.formatDate(new Date(Number(section.split(" ")[1]))) + height: contentHeight * 1.2 + width: contentWidth * 1.2 + horizontalAlignment: Text.AlignHCenter + background: Rectangle { + radius: parent.height / 2 + color: "black" + } + } + Text { text: section.split(" ")[0] } + } } } } diff --git a/src/timeline/.TimelineItem.cpp.swp b/src/timeline/.TimelineItem.cpp.swp deleted file mode 100644 index 75e03aebe8a2b20a05aec339ab9cab819ed7fe2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 114688 zcmeI537lkAb^ptKHz)ygt&R z#MJxDZ(i4ZcRhDK=bn4+-nAEv@9@sbt;_HnlgT{&_~K<}ZoDM3awwCjmW#DYp($T_ zKC^zcRjaM3`ul1<{XH#+aCCERUwfWF{%FbH?^i}^b-%i%S!*|n{%CcMXhxfj;%KWp z?^nuIe{?GNW4z_h=Zf`u&qM|v2NW1kppyda*36o-9+5fi)OBm+<4Hp&ddEI`Pp7|u z%YXs{3JfSPpum6v0}2c%FrdJI0s{*C)>5EV|AWk*Qo8#FB^(Xj_es1T75p~L1AiNN z0dMv9*x>tPf_EW!>-XOeem^L9A4q&ZHu!yB@P1n2`|k$7j|<*6CB8o-`2FzU{kg>V z2M52858hu-eAj%R6S)5*@m=%1F>qfS6imzaz`%V?@NOo)KP+(n!{Ggz#P>%A?phBw zCBFYb@O?aZe<<<&cLMj*g7-s$hyrgS+A)uo@7%=qM+Dy=8oU=0-+wP~Umv`mnfR{t zqUHEN;=9(1*5f}VzCR#{SLyV%#P|CL-_H-;-%otkdewYB;eOVmmS4+tdJuj`;`^fm z_p^idGZWu~pBZfj?@N3Sf(>2+3JfSPpum6v0}2c%FrdJI0s{&RC@`SFZ!850rA+1_ zFweIM0}Yd?{QoauC~pMs0`uS$@O7BG_kwqWCRhjVfN}gFct1D*M!{-u9Qa-EQy9N5 zgTDvw0Iva8fxiGPa5*?1oCb!#!@!SW0>1?Q1-uLVC71zY;COHxxD$r%3*htM8c+g7 zAmuog_a^W#@L7Jp5S#DPXxzU>jHqR)AB$ zvEWhQ7+{`5883L+^-`hbXNM6Ps8jS&UG_Ho!oG7t59-|XTT``KWvX1y4h<25x)%#g z-G^YzwZKJ=*}mQJA6$PP`SO)@mxUqa&d@YC_D{iZYF z7fOel&So@U_WKRL)PG_{pAd(g;FfB!u~={QpI{guDq?e?+$zp_*``$Juy-(OH^yb! zJJ&ORoRc}2kxF;kKt4~45jQ`Z_+lJwxZ%wHTDg?TM6EFTt4nj~zr8IPm8MWHl&h`r zVy&93k9eh8dvC?xH0Kxh%@-Q`#Cf{ER%luWG}34fFa6?Sr5rRBCw0w5m);gDbEQ^X zPC-)|%3+punF*VI?vydsJf~yrv!0{*?eeQxG~TM4YL!|;YB8*vT~g(KV|#gaZg;y- zDYq84R14I5!<)M@TX*U^@aWaEt+{g3jY0|B@LTOhH5w4hGeLi$cbwa6WE@WGG^Sp* z>}NpB?lx(gzr9tRJ!Q9TvE>8vh5B&Lgv+j=0XO_&D?2>m?QZ4s8!F}5>L$_4Yv3RO z^7#vEjrl^wEr|*1H*z!ON@YSq*G;53jj5hPn=d<@LwB&&$hu>)hJ@CeYV-A4v)n4z zsyk{WKghk`$mbJZrYDVB;CR)cvMnmq4wp>n?722(uXPp^#q*`dyCr>-;$Qb^5YZnxTeAC$vi{+&&ns3?rMIs z(lKtjqR2@3TmFH1!=oE{J0`bH@7l0=^Z13^Y^ONaJ9X_jCU(|!CM4@h^K=HB7~i&i zYI?_piEZN-Qm<#)aI(1}rPpwolgia%rCsv9q50N<(Q*kom1oL+qnVql*N2?XrE;NC zn{AFx6c%>)&1PZN&&^S;D14}dO0$9L&C!d9t!6Y)@z}%P_&kyI;ZIsgAKo^!OXXh4 z#&l%kMQE2tr~Cu03krMvij*$OG+%bvp{>Jx!~R06(1<(ltjxz9b`bdDa}Z9wt4+T-I+l}& zo?L5t&AD2;Qu5{s`+XUT%@WQlc*RS=-=YkCr-mX0pyLL`)p>UHFitI>&d)qDV;_<2NJNHa^8!mjZckzaa3F3S5 zIfg1VzM|D#HJLBZ*DGbeA!j)uKLbDZKg#= z_*H9H;cf6R;`vl$6u>)8$sW*n1oB9Z$pm5WD6x^kd{aI4Ab)|@AvM;Vqg;Cz(@H)g z8EXa^%S&;*VxdY~h)5!77pkREQre)quw9oA!@==P@_VLW>CGlqnrn$2|jIs{{Hw#STA)2mi$bMu8NBK9kqB53yy6n!Jc@K%_; zv(51V34fKc?G{2F*-HybHf_FV8-7#d@Q;{66?5k$eNZ3e>0zY&}Vo(S^b9Y~+w4xS5S zAOBSF-|W}xKK}Fg`+V>u@DOkd0*&W`tH6`N!@@G{5v#yAGi^`2V4(?ZvTwb{RG|*0QU!<$WB3lf!xl*IBu&Ys~VXz0B-90Jp`TVXe z({`_5#9M)gjQ`on2sWm)F@@V4;g%Hy%|JM|4xarED#JV?{+Eo18wE6v#z4%ua<$yj zEiZMqx!756xuR*a&%3ATr@1w(%sFfO2f-W2ki8Yi{2FvXlRk~dv|b2l9?6rqJ@L0C zzJAMbwEf-9bY?KgFhH2Pl(r_%Nq~-N+_c?|@SDzP+~h4!vJ+@~>+)XPbLfr*T8m)0 zzlSC|lh3yTyCnJid~>1z@KT^&)({2Cws(hrny2R%=jhiE*s9ZYJ2K+KDjmM zmhO_Zmx+48%JWuwD^`S*$~9>Nw6-yC-Ef%r=%_cfbMwwTdo;eRGWN4;P%=ThD%8~- zD>h>a-X1bIUh;|<3BpQ{cAfsRy|zoXozQIXR{aGpm=qH23|8JC84T|*Dc>9I}M zquY(7eYRzxSq5W;N5geTD7Uqw^mBATQhJ$8|>@h~z{>k-;6{BC-nL?%Mr+5n{CaTYgcSb7R@!U_b z=4xyb@0^*5mFq4mPOH(r8%o1|wo^%o!Z`Gjp{3nuqMs#tU%O-pz*>7dyUf(;&mRi2!~wTK|Ou zZnh8_h(ZE-1;K;gNt{G0KuW`ykJg)4Xf9TZbB$WH)^1i7J*Hw<_PIi{y0V2D1G*79 zu#~(-zcrFM$wwI)&4}=cO>-fc){NPP&PuV?C_jZpU#M&-?Kd6Y@g(XjECrleaXwOj zt>puLiG>qtaAyDiPFSdOWjPN2uPg6cS(jZ0Cc$d(E9&QF@FK7t{1E^9fP{M`*a*%6 z|3O?I0@s4S0R?a>_yrOF9Ed*upM$;N954!UKzaU`P=6J0IhY2=fPbK}ZUpZJ?*i9> zMX&&Nfs?=k!JC<)-U!|Rt_4?sIdC5M0d3~{;8ySsa2>b;jDXePd$gr@0@2s6fIRpQ z*1bOfp9fC^PX!0TejqY|39teD9(WA+4DI&W;5;C@`QN9_ihllgz&F8l;BDY9Km%L} zL@qD}&IO{k|7&CbzXaa|Zw7A!qQ8GFI0zQOG!Q-hHQ;-!!@mQ*4ZZ;$f?NDaE~f7i zp@BBsC4~X#+=#l4sDMue?2~A}5xhQn4m zBd6ox(PY|A@{v7cSWTPfkLNxsyK)&>S*u&^Ll1cRePG-A^Jl|qPP;N=OEG;ML4jp+ zf=7&=j9}xOl_#c9H#b^&XsD-rPp?2fvbCcg9+G zQ5?^j9&;AgxS~M9pD^XY{(Qr)R|-Xc%Y5yMa&~4u$6;TD>_)8FL=s?*-yxUTBF+=C zB{bqm7J6{>tQkzh5D~pBAkxZ~D)E&4|1ZJoz7ZZ+Dr1eQ8T9?(`F-#x@JsmpPke2hw+2W!AB@cT~#SAa9YiQo@`@c%ypH-PJbY%iYk(xQlkl`VZa7%clO4fMc7&_0qmi*g`cG}EM&?xjT(xW(e@;Vi)t8hiQT$ne|qc-lj-h^?86g4?4KgkJLBQxD#&f8{9qO4*h z*5p2oq|gzYR{A_ zEho{ZTZ?snIvYzSDAp8Tf_O!9Bku)90kUcnKy#h)?yw&q8XpVGk1C5&iS7 z9Vpp1-A0&bl3{c3R@vsTtdacaG|A9<#aOl=hAi&sCTa3BXp(es;s19towudu!~buz z{XTO&Q~NW(AA?cw6?puYfjMw7SOIgT) z{vLb?G(Z8I27X3gxdZ$V{9hpY|L+Cs!8d4xe*?CGA2Tw24T!zIzXmS_bx;GtAPdB1 z-zW3B*Vhwr{X?C`x0+z4I@UIKQ49pD0RGxQLeoI}_l@Dsv(1H2tv z4PFMG3!Vec17qM^AZG`@O8D!+pMpv7MDXx%VC(!NK6`e4@8xnNVbkJ`#}_Eyr;kHJ zG!-brp@6iA0=Ia4Jv^p->Njk^{!)?<9Zh&V9e2}wShu*p|7mo&`^vivI>P9t%!4Sv8DmdlqB!T@)3c!DW-tlTg1UG6#Q4y&A1t#yYFqGDnYOU3 z*2p9mV-!~KTPAQ}9(1wkFbYSLzwW|xgpALaW!>-Twr%n=Xx#Ty;|`m{-N6SJN}`$@ zh(`3Vkrf>s8*~ckFy_&&-pPBKh8;h!bdJ$JRt;p0L!;2Q>hf-^l@_yDT$?X&Os6!x zm!Hly+#ydki_w+WVEQKAJ&_KdW&8*ysj`laapQ7%RC2~ki?3ae9SOmd82gQj<`_t% zu%_w9vLEoBVvtRPjEEJv^fJj2BQb;*$P_4?~4uB$Ko~P0`Z70`WtV)S>{c z9H=Zc&7;x9vxWUpv#FXLnm@2c{$Au9_fV<4-!(rS4?15=c9PGoeTF`_#D={Q; zXS31VIPb7jhdQb4c+)?FJe2%qu~Fs-_(-C*3d$AtOs^CE|0?+JUjyO)^&Yji z!Si1VjsyP&um4T(Ch%wAS>O`z2=G06{0D&8|1X0rU^Dmwa6A0I$N)Bi4M5WPckn^5 z08Ro!;3te29|g|?$Aja*ztion0at<*K=%ISod0#;89>hWkAt5uEZhlh0{emL^M8SH z@~^-Pf!N}|3JCu{1AYWQ{&KJlJP5pnrBem`5!eVuz$)-S@Gr&?|A*wR-|2X_-=b^zOtoREYvX~;Y?bR=VTBKgSmlu@tq7Ui!S@1WMt#? z=7?TS!N@V0aa&0a?vW>9wCa&InhWh+dJ5stxlONXHB=Xhlgm4v9de4 z*cKe)GwGYWNI0JL3+CZYp5kJ)d_R(@qpf;`E|e;!&)n(u7Qg!M0j3&nm6x zK<3drB)0;@-h=w-e=DH17+ZKB)~3`c z4mY5Zl6{G@kMP=qrOAzCbhFLP(8*+oq2@5ls`Jf|Q!Hr?@<>sWRGjL*?b~f&@>=9v z1~eUCv^`5(RyS$u@|*#LEHUy})5%-Kgst7&YsYrvt^+=``aHRiC!Grucb2f`uC*pZ~}F_C98IGGK2C+9`%+YM;jA_=Sm(TCuU?eBNi|&co z&R|;$^X1B7b}01iuH-wtn^UaTszu_XYvRIN<*m(~O}RoEFm#9>?Z#HsR-_mLm!nkS z*)pd+CH=}x0g|L`pIHo8x2(goH;VZVv6SsPxC)$UQE9T%=Awa!c?kR(=gyl2?EM1R za!02Xy^TU;jbEvh>&}~|>BzGJs6TB#+I*69G5yNW*db<&vOVfYm2>5aAS@A~%-f7#s1wzF z&||h@KYXL9wT{k0&dgbP!(2*p!Jty)J)hSSSSlM|rp6)~+r9mx?az?Z^wX#w@0v8E z9;IHT99nM~O)4ZyV4-HomLemI#H1}nu7;Kr#7$eu$^4d<;-;9M;Ii6DJg;smnJ|(A zx1)xBmu?}k<9jmjF~-S|?`n@H{cMsZ*u#|19Mgv1h^+abO>h6XW0-3;1YvI=19Osc z`iSedOCAsBfEfDshiTI!Y(WL1`eie3$Ko^scKS+^aNTJfJK!HLp?nyBu=Gevz`2yf zAyrC%WLmZ|B)EGuiA`7=YAnaXOvu!{d;7-P0h<*&K)^7^y2In0rmyQQjWM3FZZXO# z-|b*&F$(`bO40`<1SDiTo?{X3_h<4qOV(0%w9pfZG@eUJtGWb#M`Q4EP}<#p}TY*bPnrcfyw3 z2t*d}bRc*4Jr3N#%Kmj=5y)MAqd->sA7>f-k3ekxy$XmN-~{kFMw(l|A#fIW0yqPl z3LXU>0Y1-IcP)4dxDY%R{E#a7GWZC1J9sO&20ROhtY8CpFnAF7Jj>x%fU7|f$h~{F zvz-19a5MNcxEhG;Abhl{kEYll>2srw)_F4Q}`uu&MVx zHZ`M7BEkC&*iXZt8h+GW)zWifT(5Al_XPVH)>|?|OR$<+-N|6fVgr(aT}I$8XBQJ@ z)mFp@EyH!#nZQ|)hP;U3v5r9c&>0a%q7f4(B@dBf54i(;M#XNqrVAXRNSwuZJO_Q& zc0ACs>N+0Ot?7vY%O|~w&d}Y4Y^pKK0n6vyMj2=MgrE-lEk6Uc@1l_H6GwTL@@LsY z_@gD6IUAqH#+F(OHZC0uXNPD@111Kc85W*$cU`cEMwDUf=Q#tC1~gU&Gx|&4D|T;f zbKR|fu^7Jh8|#C0-B>=aF-JSjaod~hfH z{)d3v0ayWh!R25Y{4qEdJPO6l?{L1CIrV;QtSTr+_vX1}ni&kpa9ITna7$$AB9s!~X_)F5o5n{Q@9m zdo*EA0gnI=2cN+I%fLlo4-g##kt_TXzrO%q0bd5Ug3p7ugSUdKz;3H}X;oxl$QktbXYE(iAo zpF?JF3wRfJE?5g534Vsm;J?9b;KBUvc(gA@?^a_GO|4AipnvsJZc19I(=hJaxmyG8 z=1Pj-YK?=3yqQLAp4&B0$}-2AU4yhK<4F{4f0t{Bl!Pt8m@V2Uj&_rw%b2e=yH2hG zMzqNy-s!>7iEn$blkup6ghz?d6J*TF(0>h#l*OUX|=DOGXgnwll)wV5-h8N@} z%ETod;UNt{`7wsEXC%1B-Ito`QrIo9K9a$z_3&!}#qfTyL@W3aNV~ zkjb-&JS1*mpFiEiaJpYwAEh%U${S%QTl*$#Kwp`)gFmN@iE1sELnkMc)-a*;&yHZS z%fZ-T_Q>e>=1H0iOq*x2!?txU_x=MRNm|7#sd~J7jR$V9IOl>G%W9u?G59Y+P6m8K z00uVmFv*w=ERj|}IB_uD#+Q~xnj4o^cch!AOU3(cGzBGkr_-x7 zT^*@{XwN6so-0OU9X(-q+lebwDp)fn?b_NpaNRO%k8%$pQ3%$5wItlUfob+?X^BNUk@FG12Ou+C_6m5^@N zPHN#NtJ&OW6|H4mWA?#Lm*LS7QONDyav2vZweY};hfNhqmGo+QMv${)lhXz-Q>{G7 zCf7N)MLj)Km+aZo6TG35E$TI+rPa6F$>M!KOwDNy8Y<}fp_k6S&!@Y*r<=~VB*mrs zNxdzf;!U;v(uLk-V}Qn65gsby7BPZskio5QLWhZTBXj5>N#se=vL<5Ul$J3_3aqO* zt+XQ;Qza==%7EHoiGyQQ-7gwR+Q=CqbDBZv3-YW1+Ph#=&13Zc@puwEvGD(L#7Lf- zSdx{%G2jw*xChv5v?EI#U<)asMKc4fqi4?;&{O$7%)Le+onDT@ zso^MB!lHDOb#&*sw~oAMl)PIS!r`5r&-&+)ogf?HQj==SE69P2@(c>3BJ4;xnWU-l z4yc@QoRWX`L%vA1n^@;S{(~V0JrWJ_rka99jwTqMcS_10OawVI@m5e}GC>XdRz}R8c+D+Fm2|H=I(-hf3!er72E0@wb$4f+jvSZROHYdwm=(MF? zE6xR`%eeD}`vqxX4K#hHTQdDA5u9jk-N=U}DAUn~LyBvxeMF*R+%hiT1RLx`Y>^Qa zZ277$-3~G2^B2;-(K*i?slhatJc++;J*fW5@)To&p<^T=|8TkQK>8Ic;zSS2bGF1| zp*6EW!G&Si+<+Vmcr5DW+?nbBML#^Dkvx&H{A%^QzyO(PCEYCRB}mxoXQ)IZwCdsU zu>3I{r5l$!kqMS%h>=(^9rsAL-%p2?zaEjt;=8BH(KX#38MpnV@AhnLxJ1o)na26< zRtqqP(_5@jn4X=*ERJI4{`w}K7#zV0rP>`iN=7Ipx+5P_^;n$nQ>=*1a|-{nx;8u( z-OfBqHl@y?cZh&2=1nxxi{)7j)UA)G`|Dmy=52X36%N`HWEQR>MhACNh_&nJQ}X;@ z`2P2S_ktV1cj5294Xy)k2Vw`{N#FtC|G@9R16&6xKx_d17xDcRyg}o((GLC}P+&lT z0R;vW7*Jq9fdK^u6c|uoK!GJFpsZBPTF1rLe5x_C|5s#7SFuNjB!3M;CE*qwWLS`EPl0LmlQh@&Ku!<=Ke zl^x0t4PUbMGW~TPzt&wA3EMRW+05!EaIPt~PlmlQZ(U^dOC#a%?bOcAJM##f4j5G= zZ|@?77ERoOhXlGYjtm+Flw4RB9m%3R)@okNu3C18tj3gk%??UhNv$z1l^ZG9lKM-! zB`5h1yO@DsJ(M0vWf~Rbgh1b+O832Z*l5#f*jhHv3jSAVlh*C1<0hGvXAC6$ZNp8% zNaIz1NnI8#jM;oL`&_m?2}>{GG#}CtgdlEQ!v7xwzdi+o|7R=RJi`CagU`a>-wZwt zJ`VmIJO}(B2=h;5PR{wi9Q>HS{~vfOcnjDERL3GQT+lk@q14xR>{3J!w(;L+e1@K*NO&jz2x|HFyrCT-^0WTeHI z(=lwjCC{*|8ADOLT|vv)Rt#k@n+edzJfc4Z9Zba#w|Ugf9ceYqKqtOk7ESMxzqdV` z9nupu+w3O2p4AZxdwHvOFFBO9ZuG)xm8o*w81HlR(a2u1qjZV3L&w%;T9gr_)k&ry zlzC(ndkhdaD%L#77Th_Qb-HNbonkOv8{1Nob3Q8ML%T01?DZ?&K}&ybMH=!}n_<@0 z%W7}v9Pf~84@sl!sqcvRt}k+6OH{8e+1+{yuKW2CHsvgUx7J^xZ>`!RiG@_0>5oX(XQUlx)h_genyG z%{FT7YRObMty`wPn8CFu3j0?YVZu#)OkJUFO6|59g=UK$wcS5pW6mVwF$Xwi+Et^_ ztLNOZ!(&k`o$EQO!O7rMby%HJAuF3+yISTfiEYa9DKhK;s;sLup<}lU<>Dqap>CRD zXTR!f+Y5zq3mXVqgVTSmK~RmRGf=UBwnD}AD^0!AU%5Hau2vytywO8}O(q7fP%U|q z1F;vn;Cr0d@Cp@d1!Ax>pph&g#FD6qJl8Q~b)Iuu=4d6lbUjBaXDz1$>Gz$kWxD6)t4!l9WN_TfwymSsyuv%~VmoT`*nfgDB@O<*%h z=JV2wf^bvGbC2OU8|@$F;sd$f*wt$?k(!g}674uze2dD~r zEB?mC9Sl&u_f$`=ah7iz8?^=U;e>T)YvN}*6>~{p##8T5HjAE6;XOHrY1i5)dTWUV zoe7!Ic}rGCI&Z0)v;=jNxlHFboy&%BOOB7Lag&MMU1_PC#K(fgJZ2uG>F=yoFh@(1 zmh8wtAFMNtRobY_%6iv1A*#y>$(7q)!`jzYkZNX9Y(x6o^cb^cX&gD&72KH^4F9^I z)xkuj1aXr@)$JsH3rXCJjPP4Fl*CQW0LZfuh(95XWLzF~6MyQac{f+A>koY|yFgp) z;RthfKyxoS(A=xr;g(2;V9mXhNQYo`)6C1b$iyG!$Q-zkrJ&ndWDFNK$&j{|Oc+CW zsil}+Z#CM!Sy$?3eJ`6z(zcxbw^Tg(UEhbBo)TZ<(Ra7_^t-;h`RmcVO*xmF?mZQs z+fcQeiJOi<+Renx?PfXxr8U45nwjxn*gNsuq}kX>`NDPd@fMpL6hw0zrpvTq&Dkn` z%K+f!>^y!ayGwG!7Nr4cj9n&fDUgmKnpBddID*Du?~Cm+_ARFZG>@qjVDz8G96YRq zEH0b5{e?=~&kiBlm>ZSZnfVs8{jFMXme&3mowg;D2;L3k%>UWoEbt&8`~UX`XEI>Do2gOG^M92D z_k+D)6#PDT4EP|U?JI!D1LnXC@WEbiCO8cg$n?X(^T?>^=syB{iRtyj;6vbfU>n#9 zjsw37KE(ieIk=7H_jPayko)`OeEl21EZ7Vl1wO}A{uc0N@NDpBKy3AWhJxP=o)0bs zqJMunI0@XzQs52X)!-U%5tss#U>9(oz7+ALVx%(MPONS+;7cW$M;mdp1W*?mO{c=X z3}^y3UUhTh)kyRmTOh?%pPlKTE0;`O_Rw5|Q@>UyN4SheuWys*dy8Vf2+L@*hL*H3 z^QfE9Qr)z0GOH&J0O%Yp)j=~dkGe?|>J}18>p`@;7@#>eAL@vw(NmM=9s29RVB>9Y zP@us`dxQrC812Hvy|w~%OKTZ-$$H zG^={xLHsaZnKG*%b#udYS8nY8BKf0rFhaF>C4bDR<~fD`weEDlQ#bLaZd(47cYV@^S|^FcN|u?KC{rD2Jnd_B8j?@_R!R|uRvxUjOtF;O z>gLvftn8Bh)Xnubg+EqKF*2!}TcY2{LnLNxad>+1=7Mw-K3c2l-?9L-K#rQ_hKOw?l9rh>x7i8{7~0^GNliwn@iXpy*g}0-m3lOl3$B% z&XL;cx1-8XFOS<%)lF}flK6CtQ#Z+^x~(L9vO}wzTNvloo#oGX9)}rA= z<#*2>RlM8wb8nTs-e)+xRLLc9zjIN)=^Sl;)mD1GwHv!#yGPo~>Bc(exls%M-(Y_J zW0@e~|4)-f$MXdm-Ywu%-e!Q;VWfu8A?a=n<}uLce9Ctw5|4}KSj9l!74=eyuW@E*_v>%dy@IcQS@Rq$AF zD;?@f;Emu}APXJknOAwoXVi%wcX2D7z_XwN}{t)~tGr!mZ5IX_(Nt3DVT&M;Im78Lq(h9nQ zEfKSY+2nvUyK5@~CceXLN{ZM}_TankBusL&oF#QyX5%T@t(T=G zx!w~!xlnfq#gt%s&k#FIy|)bnP1p!~-Ikm<{`))~WNWP%0je{66HPysZmd=^YhNQ>VO?pSSfnkjSDQDzUf2q& z`!)!}(!4`1n~9}kR7}*xA;VoD71vbmHVJrGQafvzbai^xYthroX??L$O_MciIaEd* z$(6%Klpd=FxI2W(n#o=QA~Uoqc_DHqV_+>!!VjhMN=+E-+PX?mVTH) znuS@vnKSnP{}%rBE%3I&|Fdmto}~T1PcX3D0Imlw0G9*N1;~Ll;JfhrF99zGCxSP^ z_siM;4d4fid*26t2c7{|fct?@!SlZmh%Ue^*Z|H0j{=`&Vex8k4cHHSa3&BLz<+`t zgAalafcJwUD1dXpG2oFvbO#;`KE`F*BQ0#d{?gJ2rHBs8dL;23N!YZ!gqtzK zKFjeHnMRo}%(I^VWlj_~dCQX&RUWQ1lzGhPPfOg)b>?|2|4Z6#9!}=BtVhL7m~46S z0GYCJlYGmQMwm$}U3EwwD{eBWWYIbWt9OwUwp#|p-KB-7o3fTLZo2$gT7oN-9*8G= zBQ**5>X`6Rjt9;cT4HnF$U>t-SHG>|iQfY0uP6&IL)Dj+u6>k49LO-{eR~(f_SSr5 zOS4$0%Wo^A(Xmyj79(1`w;=mDLbJdyPN8y`CJ1M3pMea+NV2p-bYealG7OQZA3e<+ znWl6~I)xh5Wdqx?=FtjGdD}V;PucZUZCg!b(nut^KBDeVWli?~pG2%HguKkLYfX;f z*;!kB4_prpfG2=&&|JR`z6Ndu!uPKR--qA-9(WCqd;d-Vj{~1$zWyxu4EQ7{0lD)} z>;!xn+zPG++rc(43`9TRXRL|-4#?eqaz@}%@IdetX7i7N8rT3<0S|nNyk8BT2@U`$ zi`e7;5D?q^9{}$KKcy+%3FO9x=Y#XXZB(4>&0hmf2M+>wK;5fA5nKSi&06FpAZGwJ z0@3|{7FYxa?xVF@EO4?>w!+)Z@oGuTsMi{1XPlEWa>zSxdb%vL&gmyLmJfeedN&?7nEWR%i5V+^cP65s2Z8$X}04ir?;egj5}Q6t|<1N59%{9_38X zJ(0)Fex;1fb)WM%{Si}jE4pWvHrhc@aXWH?hUc{U&1@K;dv57v9k~^^wA?m122q^z zvcetta$s1FplBOcR!ZF@0(HxTtyn_J(MA)6JMubV!!^P#u-uO?g-m&8fpI0+>V3>zv=O%)sek$O}k~X zrFNhAW@vB`yH5>K^T*?M@4PP<}*3ZDa8*j zpWJ$V7&)|RdA)L}gG-W{n%jP8Sg944L%5VlB1yaCB%!%EOxi6ogJ%757&RN4uBLUq zN=`Q!Elgdk#O?B{*)ol8Q;jRhrR9a6cS$q#8{5mXbGzFGY_2VCsfy`~hBtR*wvJCB zlFAx&zBmat)|qBrkp?J_rWZu$aTd^C=O_^<8=w)$Y$_9x)QCn9#-K5!UA$)|SZ3O; zXTD|SI&3TyBq<^`osz=IHMk{vddg&L`tlaJ;WxY6l+XO_txBI^Wj2)=UJ4h?rkNA? z4W|&BiIXDC-FCDnADA!HhjS*)>ezF`ZaMN}z*^y@$XdBb=3fC&x zlqRD7lFcD)&d70IPwq=VDRfkF+YJ71mq)GlHFC$m8Z7TvelAz9&bkJ~;^TXd*g}sT zF@&RBuh8Nt8M2>%Za9&fbgFC!|NjoiX%^w||7Sx*d49#z@>wAE|8E5Q!GnP8|Gxt~ z6FdXxJ%Dm9;7srYunv3^{{J>`6L=~(3w#&e|IOf1@GGkBjbH+t0B)y;{{Va+d<(n; z{1w;=enC_DIk+B7fS=GzWbgk5@J{d|a0ryaT5t1sG>uPykAsNm zFvT7GH=w|P0s{*CW>Y{{a?!gb*F-0tR%4N6a3_@2uN8qYZ4g zRxwzL@z#Yxqngch`jXvOztM=9+lzczE6QTrvRN+_xv{TxARinVh(x{OU6wSc^hclb zdDf_6j*nF+#|u<}(X79l-U`grVl_a#&$U{0u>*$<%4Tazqu~X)i866XN4ZD5dZTt= z(a7yeoL#8x_p`=j_%gSQVYo~bS-A0`PB#5wr7TPWgyM=#dz*STYT1WU2)%oH&xLy? zw`|6Kb)iz5#fR`lEDFrpkVS}jG!=aZJE9VT+>53+ zT&az*meYra9rG*D-k4rA6v&Z(;W<_qN@I5Mz+zBi6G6)=S@VS>UTAxa8FhX%DeH#|d88=TA51u$Wm|AEh&le6O-05G3lSFQEAm^u9CVeV&XEYgNw@kg5SX8xJk+I1}r9HY#@u& zkS~?UUE>Nr#D>R0$#zdK zd+^W*jXVT~dixv)qi|jR>Qbkj)K89iZ1DwD%`R1e zQqU8lgd)4rnPh(wEg;!OGdomu2Bjssl5{6m@{;UINH!efUt+cnE3uA1jPLDw52Ifh zrJ9~^lGx;6q%mfTEn8P#$&GlqS}bAK;7&u?k&Zf=2@|RHX`Q%5!EY8D<+_pEG-s!C z_G`L`L}VCBFc??!jPSq6B!q}C*yhpXF#T%pNNCR0+Le;G*GD>4uP~=$1DSgdj7?a^ z_&SmwQ@iX$HQL2iE}8^2s^o%3T7+cPl78CC@L;q}J~VF!jsE{Fh!JlWN|3$tO#a}v zDaPx;-+~(02#y7}LA|$tL*S`EbO8R9Y4xwcMc`QQXz&e&<*$RUfm^`~!1KX0I2GIv ze3tCI3e1BF5IMj;?WBk=3uj`@w6#OTaV18$suz6{zclj5_q5fqH+{IQ)4GX`7Za z;g>MZjGj|CO1Pa#{dc~XGodBhU}tQ7yRh6`GlphjqkLeRziW-0`G45mfA2f8q{m#L zF`MnOn#&|TcmHjO%VH}qdTp46Y^#wS3Kop?<=~rVISH$K*hdO|&0E1VW!GG0vSRsO zXI<~JY~3Q#b#7Q$(Q`eYESo7&iiz^O#a|*3lb5^0Ev39L9C9c6em}+_g|l#A>_+UN zTmC`@1R38M_{^i();=v;H)4IABc7FV)f}u3m;bg~hO2+^dRfv+x%Sx@KiF#o=w&e8 znf;ra4VM%f6EyGo?!L58A6iN#u?07BF*v__gW)yvd3y5ftfXr z1TL<`->y{Zt%ltUW44Yhax|Q*hGbzGhE0THC6OAgr3bS$U?LnVl+dR&!5l+h| z;pIuL)a^mut~{=eIV^O{lU$h3vlD$mVX@Y3*{)>&<6Cqo2VSnr=qv6aQlFbuinb z^CE(=b6gP0**l%8$) z#o<8PXTR85YXtqN`?Z83oam6`$o{{_O!Oj|jo|-}vy(~YKjHOX1IB^a0Q@<8zT5@) zN>B!}&;Lc1yKe%ggD=3(FMuuJMBsr>5~th+_*#uWhK>v~ zO+vO%R68t+*Z$T2_C+MS?|0uq(@N`o-L}%$S`rDnT;?((hYDyHS*-lhe??I(+5I#H z9X3)ok`fIp++gY0Mv`8{Rd=tC~JO}(4 zI1iiy9t<4Fh;AR6k zS_E?-8Zn}>?Cu(>n;3sl)ssKK5BjynANsWBQ8(A_jtCgo3yk?wH}Q8Q`8$%Zy~?5a z6B=mQlA1b7Ph(~iR*f@Pp-DH<0y3mcxI2^{3C?MSVT>uxu8I!Askt>W#BZg(P*)xi zAgG(>S}ez^oBG>?Te3zjO7S<1oARa_pHZ+74fZ+TlTu2f84Z8cbA1jov7Ztr=_8YL%&SU5;`|&dm1|(VGYz zg2+N|snHDQqN5rPXRaB7x`k@3HHZ4Yt2^q=i-M9jSKvUR%`7A?SIudGdB0GVO2}n2 z5+eZ#vk_0w@c$pBMQs#;AASAzc*!H@|33=e2Hpyu3Z4QMK?{5zzW=-6X7Dobzrahu z3xU`K90uPe&YOX##Xbpa2it&2%O*4Ue?Wl&1qKutP+&lT0R;vW7*Jq9fdK{X)f7nT z-8bRc(%gJ8>rvt))2>FDZF1}@%aKg@yL^7vmT9Za%tmaj-r|3Z$_3^R4Oep{@%0<_ zX#4B8Y!omwrWZ5%C{haH`;~VWBY$$DSlom+mq&R%sFpM5hg_;j%qAB*&1rTIPgcgG z5{3KONHWLsmcQMYlQIuuh|rueO|#*cj1i8p!-k_sh*b8VDJO}pYtCccnO3v?R4 z^*~xoaN_&f)+oWrK_J#JJ%$&x&9EyZ?X6T688ckufOy< zQX#%CmXpmqGG4N?EF)8F{+6kNkRsAjGRV1B%+3M) z`P6%l$0d`ADLp`@(Wj7cpnJld0;}NOBszR9mP{ znJj0`)`mUb%`6ftyOWD;e${Vq2ZTxA<$RpF>GZu4H(9%gKdCEuG@nPs0Ffph#ahBN6Gw_S7`q9QSILvo zikZ(mnunuv*|V7VPBRXysieGj+XcF}s*bt2Dwin<^`tQF;^!28Gw&KV?BbXwB`$Sh zN#^dH1valnGD~f6^$~S=84_0FX*?sD6UhuqjDEAGy;_>#NUW@^Wz2Ij|LYD}9~nxN z)h_Y67d1&mcQ+_9<2c^(=d&?ukIo?JwaB^f3p(a#8<+O2ZqnA|IRi*3q&CCStQyUD zt2~aHs_luUSHy+{Muj{KO&2N^_VPwoVH+x=HHz^svFfm^Rw)-3Vei7f;qe;&3?_ZV z9!Si6N8vzuzCG`3s@WE%$6M__U>MPIBaO0_uqI&Q-C922m$v(|yOwP=x6bG0?BB!Q z8qfT$VEcmW_hP(8@(_L$6M4p-`e8>D7~YtS=d{Cw#_p{!E<41GWU*;-8CemKFJehR zs#U948>}#S+F<2v9<8`=EHyF6l?R4#+|K0&S(6^~Z>?H0=0}~X>I}BEFkh}LViHMx zyF0R--gnhVL7K_dL?LqZ=*uvzaBc2v(hX}f;O4{*gCW1MH4-=)YcftIlYYg1y(DRy zQr0y_GU}Z=60=T%gKL2*Eh^oSAbvq2USy zPLX#^<}!NiiwqYf%s81)cD8mh9L8mu;}JBm&WNyv#QGrFyduXQjSHJFl{Lnvwkpy8 z{}fgAC8bK(%MF{-b7L%btiS1oYn(N&F!+;&hhuQXR|{yu%*p8zZ5k;TM}dh+-i0R zn{m=olPYy_L*q2lU7aBPL_2!e8ifFo?qe8 z*zV^ki=x^XQPHk#x3*jD2CG$Wl&S`zjeNe*u4ZQgsfoKn)!d6$Q*I)Lg$-$;SR zt>nifFOnZ4%#tlQzv^i!9!FaX&BbbQuEE;A-K;D|om-D%s?M5>$7>Rf$9Z>PQ;tm}ysyU}$|1Yj$`ns*!WOz$UDF%2T7ES~WRC zfR#@S^dcA-#iAvM?fga1}YY+ zMZe{xt@tz4=njNC+Ocq55+TrS|V z_SRs$W}6IdNVWJd>C0s$a)IK;#T^VXeqfePzHMyO7Kqsi>(JK3AFzI5#umO1QMMB4P`w~I=V+JN2oaK2c_oeRa}SJ8tR)QbJ2{@M#!V(vX)!P&<}q^=&39+D zg2Ws7yCf6SrbqA{b-NCG{&sl>ixx{gB)rycs?8bj0%V+(g-a0`?iu~T`crc8YUZ`?44*x+G&j~88^+Pgt@xv5m~n; zrFqkpknsOkQo1)v389paXT1#mA5dUGfdK^u6c|uoK!E`T z1{4@jU_gNZ1qKutP+&lTqfUWHiA60C!7ZK;aPC+YM7pglkB&s;V z|KATyuuUR{g8v^6^8EvN{r7@v!E3>jz{%hw@FVv9?*Kmp4e$W)4+x3N;5cv}@M*+f zH-dKqx%2OAa29wFcmTLRxDWUOf(g+FSPy=V9OQq43xMqT-yhtL1m*|eAHgfY)nE?H z0uOu)A;+cQ67W5Q7Y%SFI3GL|d>ets4PZam2`&U#uoCFF-*1VljZq(CAg_dMmrJa3f#KV z;ZEN%d3{_bofy>Dp>7+mq`QYb1$!DRE3$RJLAWrWMqu*S<5pYsr|(#{Y_ze9ZAT)- z`f0PRkdo$d_07ZFi>98+e;E?ZqZt!HwwUFa4txjOed=9xe>3V|3nB{0NanfqJXGRL zqmu^X9s29BKa00uizd8v*WSVCq!MjVIg#t1LmuT|Us}#8g!XlNUqv0%MnfNsu_M=_ zcsbKTa$OH}*y9yUQl_UQYuuz8?-^tu^VD?yKls*;D z3LreApb3T)CEMurn{>lQK$5%lK!z>sj!D1RoGi2JyQN+$&e>FQHMSSaRa$2Q#n|aq zleA<~Q|_s> z+iF31!^~tfAuflTxVsO8bUQ8XS zi;9Nljf95d4+bZNgbCuhV|$)+y2XLc>8__?@_7^Apdr(G!l6co9OrEZ)L4sMy0XG{ zJekry-F}idx)fHiw`#v>RGFo=xMd!Y$343z-Bg~HAlW$_#figd>V1zL4H7*~ z-Mo)tQ#bLYH+A9v{{f1vhZh(AUoqS8?*jZ;7`Cd z@FV#CkASy<%YfVo@Obbk;(i@?0Wj&e+3JfT4mr+1j znqmPR@^l5~En=}HZ^T$hrDYbkM(iXz8?wU@u}n+FmzHjtTeT#Yfz=v;vbD*}Z;QWl znw|_5l|6%*Fprj4-%D>|wi?-?NrWo#)8E}IS^Rd-kIGDsG(Uc|-~3E)5aq7U&t{}l zNKgGz|CzEf$?h2w)pC))9vPuR{rP?~76#~^GnEV{bJk>+qd4bf +QDateTime +eventTimestamp(const T &event) +{ + return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); +} } TimelineModel::TimelineModel(QString room_id, QObject *parent) @@ -36,6 +43,7 @@ QHash TimelineModel::roleNames() const { return { + {Section, "section"}, {Type, "type"}, {Body, "body"}, {FormattedBody, "formattedBody"}, @@ -55,16 +63,49 @@ TimelineModel::rowCount(const QModelIndex &parent) const QVariant TimelineModel::data(const QModelIndex &index, int role) const { - nhlog::ui()->info("data"); if (index.row() < 0 && index.row() >= (int)eventOrder.size()) return QVariant(); QString id = eventOrder[index.row()]; switch (role) { + case Section: { + QDateTime date = boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, events.value(id)); + date.setTime(QTime()); + + QString userId = boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, events.value(id)); + + for (int r = index.row() - 1; r > 0; r--) { + QDateTime prevDate = boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, + events.value(eventOrder[r])); + prevDate.setTime(QTime()); + if (prevDate != date) + return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); + + QString prevUserId = + boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, + events.value(eventOrder[r])); + if (userId != prevUserId) + break; + } + + return QString("%1").arg(userId); + } case UserId: return QVariant(boost::apply_visitor( [](const auto &e) -> QString { return senderId(e); }, events.value(id))); + case UserName: + return QVariant(Cache::displayName( + room_id_, + boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, + events.value(id)))); + + case Timestamp: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, events.value(id))); default: return QVariant(); } diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index a4224538..41a25f61 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -15,6 +15,7 @@ public: enum Roles { + Section, Type, Body, FormattedBody, diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 7f760eac..ff976aad 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -34,7 +34,6 @@ public: Q_INVOKABLE TimelineModel *activeTimeline() const { - nhlog::ui()->info("aaaa"); return timeline_; } From ccedbde38b312f907c2845132ff60f57bcef7c08 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 1 Sep 2019 22:34:36 +0200 Subject: [PATCH 06/94] Add avatar placeholder and scrollbar to qml timeline --- resources/qml/TimelineView.qml | 34 +++++++++++++++++++++++++++++---- src/timeline2/TimelineModel.cpp | 17 ++++++++--------- src/timeline2/TimelineModel.h | 2 +- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 7ff51362..3697b37b 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -13,18 +13,29 @@ Rectangle { } ListView { + id: chat + visible: timelineManager.timeline != null anchors.fill: parent - id: chat + ScrollBar.vertical: ScrollBar { + id: scrollbar + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: parent.bottom + } model: timelineManager.timeline delegate: RowLayout { - width: chat.width + anchors.leftMargin: 52 + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: scrollbar.width + Text { Layout.fillWidth: true height: contentHeight - text: model.userName + text: "Event content" } Button { @@ -88,7 +99,9 @@ Rectangle { property: "section" delegate: Column { width: parent.width + height: dateBubble.visible ? dateBubble.height + userName.height : userName.height Label { + id: dateBubble anchors.horizontalCenter: parent.horizontalCenter visible: section.includes(" ") text: Qt.formatDate(new Date(Number(section.split(" ")[1]))) @@ -100,7 +113,20 @@ Rectangle { color: "black" } } - Text { text: section.split(" ")[0] } + Row { + spacing: 4 + Rectangle { + width: 48 + height: 48 + color: "green" + } + + Text { + id: userName + text: chat.model.displayName(section.split(" ")[0]) + color: chat.model.userColor(section.split(" ")[0], "#ffffff") + } + } } } } diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 8a74edaf..d7eb02d0 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -56,7 +56,6 @@ int TimelineModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); - nhlog::ui()->info("current order size: {}", eventOrder.size()); return (int)this->eventOrder.size(); } @@ -98,10 +97,8 @@ TimelineModel::data(const QModelIndex &index, int role) const return QVariant(boost::apply_visitor( [](const auto &e) -> QString { return senderId(e); }, events.value(id))); case UserName: - return QVariant(Cache::displayName( - room_id_, - boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, - events.value(id)))); + return QVariant(displayName(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, events.value(id)))); case Timestamp: return QVariant(boost::apply_visitor( @@ -119,7 +116,6 @@ TimelineModel::addEvents(const mtx::responses::Timeline &events) isInitialSync = false; } - nhlog::ui()->info("add {} events", events.events.size()); std::vector ids; for (const auto &e : events.events) { QString id = @@ -127,7 +123,6 @@ TimelineModel::addEvents(const mtx::responses::Timeline &events) this->events.insert(id, e); ids.push_back(id); - nhlog::ui()->info("add event {}", id.toStdString()); } beginInsertRows(QModelIndex(), @@ -169,7 +164,6 @@ TimelineModel::fetchHistory() void TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) { - nhlog::ui()->info("add {} backwards events", msgs.chunk.size()); std::vector ids; for (const auto &e : msgs.chunk) { QString id = @@ -177,7 +171,6 @@ TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) this->events.insert(id, e); ids.push_back(id); - nhlog::ui()->info("add event {}", id.toStdString()); } beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); @@ -197,3 +190,9 @@ TimelineModel::userColor(QString id, QColor background) id, QColor(utils::generateContrastingHexColor(id, background.name()))); return userColors.value(id); } + +QString +TimelineModel::displayName(QString id) const +{ + return Cache::displayName(room_id_, id); +} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 41a25f61..9dfb4401 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -29,7 +29,7 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; Q_INVOKABLE QColor userColor(QString id, QColor background); - + Q_INVOKABLE QString displayName(QString id) const; void addEvents(const mtx::responses::Timeline &events); From 56e27ced2555fe6d2ae4644c391cc81a9b5bfd85 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 1 Sep 2019 22:58:26 +0200 Subject: [PATCH 07/94] Format date (close to) the old way in qml timeline --- resources/qml/TimelineView.qml | 2 +- src/timeline2/TimelineModel.cpp | 17 +++++++++++++++++ src/timeline2/TimelineModel.h | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 3697b37b..bab1d932 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -104,7 +104,7 @@ Rectangle { id: dateBubble anchors.horizontalCenter: parent.horizontalCenter visible: section.includes(" ") - text: Qt.formatDate(new Date(Number(section.split(" ")[1]))) + text: chat.model.formatDateSeparator(new Date(Number(section.split(" ")[1]))) height: contentHeight * 1.2 width: contentWidth * 1.2 horizontalAlignment: Text.AlignHCenter diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index d7eb02d0..6f212833 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -1,5 +1,7 @@ #include "TimelineModel.h" +#include + #include "Logging.h" #include "Utils.h" @@ -196,3 +198,18 @@ TimelineModel::displayName(QString id) const { return Cache::displayName(room_id_, id); } + +QString +TimelineModel::formatDateSeparator(QDate date) const +{ + auto now = QDateTime::currentDateTime(); + + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); + + if (now.date().year() == date.year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } + + return date.toString(fmt); +} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 9dfb4401..e2c7b73a 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -30,6 +31,7 @@ public: Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString displayName(QString id) const; + Q_INVOKABLE QString formatDateSeparator(QDate date) const; void addEvents(const mtx::responses::Timeline &events); From 34f5400e99eb0ecbf402fd5e7961dfed7b076ed2 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 2 Sep 2019 23:28:05 +0200 Subject: [PATCH 08/94] Implement TextMessage delegate Text selection over multiple items doesn't work yet --- resources/qml/TimelineView.qml | 47 +++++++-- resources/qml/delegates/TextMessage.qml | 10 ++ resources/res.qrc | 1 + src/timeline2/TimelineModel.cpp | 123 ++++++++++++++++++++++++ src/timeline2/TimelineModel.h | 66 ++++++++++++- src/timeline2/TimelineViewManager.cpp | 7 ++ 6 files changed, 243 insertions(+), 11 deletions(-) create mode 100644 resources/qml/delegates/TextMessage.qml diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index bab1d932..f0f73ec9 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -1,7 +1,9 @@ -import QtQuick 2.5 +import QtQuick 2.6 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.5 +import com.github.nheko 1.0 + Rectangle { anchors.fill: parent @@ -26,20 +28,43 @@ Rectangle { } model: timelineManager.timeline + spacing: 4 delegate: RowLayout { anchors.leftMargin: 52 anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: scrollbar.width - Text { + Loader { + id: loader Layout.fillWidth: true - height: contentHeight - text: "Event content" + height: item.height + Layout.alignment: Qt.AlignTop + + source: switch(model.type) { + case MtxEvent.Aliases: return "delegates/Aliases.qml" + case MtxEvent.Avatar: return "delegates/Avatar.qml" + case MtxEvent.CanonicalAlias: return "delegates/CanonicalAlias.qml" + case MtxEvent.Create: return "delegates/Create.qml" + case MtxEvent.GuestAccess: return "delegates/GuestAccess.qml" + case MtxEvent.HistoryVisibility: return "delegates/HistoryVisibility.qml" + case MtxEvent.JoinRules: return "delegates/JoinRules.qml" + case MtxEvent.Member: return "delegates/Member.qml" + case MtxEvent.Name: return "delegates/Name.qml" + case MtxEvent.PowerLevels: return "delegates/PowerLevels.qml" + case MtxEvent.Topic: return "delegates/Topic.qml" + case MtxEvent.NoticeMessage: return "delegates/NoticeMessage.qml" + case MtxEvent.TextMessage: return "delegates/TextMessage.qml" + case MtxEvent.ImageMessage: return "delegates/ImageMessage.qml" + case MtxEvent.VideoMessage: return "delegates/VideoMessage.qml" + default: return "delegates/placeholder.qml" + } + property variant eventData: model } + Button { - Layout.alignment: Qt.AlignRight + Layout.alignment: Qt.AlignRight | Qt.AlignTop id: replyButton flat: true height: replyButtonImg.contentHeight @@ -54,7 +79,7 @@ Rectangle { } } Button { - Layout.alignment: Qt.AlignRight + Layout.alignment: Qt.AlignRight | Qt.AlignTop id: optionsButton flat: true height: optionsButtonImg.contentHeight @@ -90,7 +115,7 @@ Rectangle { } Text { - Layout.alignment: Qt.AlignRight + Layout.alignment: Qt.AlignRight | Qt.AlignTop text: model.timestamp.toLocaleTimeString("HH:mm") } } @@ -98,13 +123,18 @@ Rectangle { section { property: "section" delegate: Column { + topPadding: 4 + bottomPadding: 4 + spacing: 8 + width: parent.width - height: dateBubble.visible ? dateBubble.height + userName.height : userName.height + Label { id: dateBubble anchors.horizontalCenter: parent.horizontalCenter visible: section.includes(" ") text: chat.model.formatDateSeparator(new Date(Number(section.split(" ")[1]))) + height: contentHeight * 1.2 width: contentWidth * 1.2 horizontalAlignment: Text.AlignHCenter @@ -114,6 +144,7 @@ Rectangle { } } Row { + height: userName.height spacing: 4 Rectangle { width: 48 diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml new file mode 100644 index 00000000..5f4b33fa --- /dev/null +++ b/resources/qml/delegates/TextMessage.qml @@ -0,0 +1,10 @@ +import QtQuick 2.5 + +TextEdit { + text: eventData.formattedBody + textFormat: TextEdit.RichText + readOnly: true + wrapMode: Text.Wrap + width: parent.width + selectByMouse: true +} diff --git a/resources/res.qrc b/resources/res.qrc index 65770c8c..b2f27814 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -116,5 +116,6 @@ qml/TimelineView.qml + qml/delegates/TextMessage.qml diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 6f212833..112b2752 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -1,5 +1,7 @@ #include "TimelineModel.h" +#include + #include #include "Logging.h" @@ -31,6 +33,119 @@ eventTimestamp(const T &event) { return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); } + +template +QString +eventFormattedBody(const mtx::events::Event &) +{ + return QString(""); +} +template +auto +eventFormattedBody(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + auto temp = e.content.formatted_body; + if (!temp.empty()) { + auto pos = temp.find(""); + if (pos != std::string::npos) + temp.erase(pos, std::string("").size()); + pos = temp.find(""); + if (pos != std::string::npos) + temp.erase(pos, std::string("").size()); + return QString::fromStdString(temp); + } else + return QString::fromStdString(e.content.body); +} + +template +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &e) +{ + using mtx::events::EventType; + switch (e.type) { + case EventType::RoomKeyRequest: + return qml_mtx_events::EventType::KeyRequest; + case EventType::RoomAliases: + return qml_mtx_events::EventType::Aliases; + case EventType::RoomAvatar: + return qml_mtx_events::EventType::Avatar; + case EventType::RoomCanonicalAlias: + return qml_mtx_events::EventType::CanonicalAlias; + case EventType::RoomCreate: + return qml_mtx_events::EventType::Create; + case EventType::RoomEncrypted: + return qml_mtx_events::EventType::Encrypted; + case EventType::RoomEncryption: + return qml_mtx_events::EventType::Encryption; + case EventType::RoomGuestAccess: + return qml_mtx_events::EventType::GuestAccess; + case EventType::RoomHistoryVisibility: + return qml_mtx_events::EventType::HistoryVisibility; + case EventType::RoomJoinRules: + return qml_mtx_events::EventType::JoinRules; + case EventType::RoomMember: + return qml_mtx_events::EventType::Member; + case EventType::RoomMessage: + return qml_mtx_events::EventType::UnknownMessage; + case EventType::RoomName: + return qml_mtx_events::EventType::Name; + case EventType::RoomPowerLevels: + return qml_mtx_events::EventType::PowerLevels; + case EventType::RoomTopic: + return qml_mtx_events::EventType::Topic; + case EventType::RoomTombstone: + return qml_mtx_events::EventType::Tombstone; + case EventType::RoomRedaction: + return qml_mtx_events::EventType::Redaction; + case EventType::RoomPinnedEvents: + return qml_mtx_events::EventType::PinnedEvents; + case EventType::Sticker: + return qml_mtx_events::EventType::Sticker; + case EventType::Tag: + return qml_mtx_events::EventType::Tag; + case EventType::Unsupported: + default: + return qml_mtx_events::EventType::Unsupported; + } +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::AudioMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::EmoteMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::FileMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::ImageMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::NoticeMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::TextMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::VideoMessage; +} +// ::EventType::Type toRoomEventType(const Event &e) { return +// ::EventType::LocationMessage; } } TimelineModel::TimelineModel(QString room_id, QObject *parent) @@ -105,6 +220,14 @@ TimelineModel::data(const QModelIndex &index, int role) const case Timestamp: return QVariant(boost::apply_visitor( [](const auto &e) -> QDateTime { return eventTimestamp(e); }, events.value(id))); + case Type: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, + events.value(id))); + case FormattedBody: + return QVariant(utils::replaceEmoji(boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, + events.value(id)))); default: return QVariant(); } diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index e2c7b73a..3af94643 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -7,6 +7,65 @@ #include +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, + UnknownMessage, +}; +Q_ENUM_NS(EventType) +} + class TimelineModel : public QAbstractListModel { Q_OBJECT @@ -26,8 +85,8 @@ public: }; QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + 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; @@ -40,6 +99,7 @@ public slots: private slots: // Add old events at the top of the timeline. + void addBackwardsEvents(const mtx::responses::Messages &msgs); signals: @@ -57,4 +117,4 @@ private: QHash userColors; }; - + diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 32321fd2..df9a2270 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -7,6 +7,13 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) { + qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, + "com.github.nheko", + 1, + 0, + "MtxEvent", + "Can't instantiate enum!"); + view = new QQuickView(); container = QWidget::createWindowContainer(view, parent); container->setMinimumSize(200, 200); From c1ee22a53eaca688ce0975e8a0732ce193930945 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Sep 2019 02:14:49 +0200 Subject: [PATCH 09/94] Fix shadow warning --- src/timeline2/TimelineModel.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 112b2752..c1918d20 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -234,15 +234,15 @@ TimelineModel::data(const QModelIndex &index, int role) const } void -TimelineModel::addEvents(const mtx::responses::Timeline &events) +TimelineModel::addEvents(const mtx::responses::Timeline &timeline) { if (isInitialSync) { - prev_batch_token_ = QString::fromStdString(events.prev_batch); + prev_batch_token_ = QString::fromStdString(timeline.prev_batch); isInitialSync = false; } std::vector ids; - for (const auto &e : events.events) { + for (const auto &e : timeline.events) { QString id = boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); From df1da1e18fc0a71e6871051c5af852d6ce8390e2 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Sep 2019 02:19:54 +0200 Subject: [PATCH 10/94] Install quickcontrols in ci --- .ci/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/install.sh b/.ci/install.sh index c1f42357..b2fa5cda 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -54,5 +54,6 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then qt${QT_PKG}tools \ qt${QT_PKG}svg \ qt${QT_PKG}multimedia \ + qt${QT_PKG}quickcontrols2 \ liblmdb-dev fi From c4ba832331e2daa17fd0efc724a2ccb0b68b18a9 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Sep 2019 08:23:07 +0200 Subject: [PATCH 11/94] Fix misc CI issues --- src/timeline2/TimelineModel.h | 3 +-- src/timeline2/TimelineViewManager.cpp | 2 +- src/timeline2/TimelineViewManager.h | 5 +---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 3af94643..51cb9be3 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -112,9 +112,8 @@ private: QString room_id_; QString prev_batch_token_; - bool isInitialSync = true; + bool isInitialSync = true; bool paginationInProgress = false; QHash userColors; }; - diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index df9a2270..0e0e74e4 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -44,7 +44,7 @@ TimelineViewManager::setHistoryView(const QString &room_id) auto room = models.find(room_id); if (room != models.end()) { - timeline_ = room.value().get(); + timeline_ = room.value().data(); timeline_->fetchHistory(); emit activeTimelineChanged(timeline_); nhlog::ui()->info("Activated room {}", room_id.toStdString()); diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index ff976aad..1bec8746 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -32,10 +32,7 @@ public: void sync(const mtx::responses::Rooms &rooms) {} void clearAll() { models.clear(); } - Q_INVOKABLE TimelineModel *activeTimeline() const - { - return timeline_; - } + Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } signals: void clearRoomMessageCount(QString roomid); From 10eb64de812346958e6a34dca3e7165207b71b63 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Sep 2019 20:24:27 +0200 Subject: [PATCH 12/94] Bump required Qt version to 5.8 to support Q_NAMESPACE --- .travis.yml | 4 ++-- README.md | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 305bafc8..078611a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,8 +42,8 @@ matrix: env: - CXX_COMPILER=g++-8 - C_COMPILER=gcc-8 - - QT_VERSION=571 - - QT_PKG=57 + - QT_VERSION=58 + - QT_PKG=58 - USE_BUNDLED_BOOST=1 - USE_BUNDLED_CMARK=1 - USE_BUNDLED_JSON=1 diff --git a/README.md b/README.md index 4dd4c121..efa37e89 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,9 @@ sudo port install nheko ### Build Requirements -- Qt5 (5.7 or greater). Qt 5.7 adds support for color font rendering with - Freetype, which is essential to properly support emoji. +- Qt5 (5.8 or greater). Qt 5.7 adds support for color font rendering with + Freetype, which is essential to properly support emoji, 5.8 adds some features + to make interopability with Qml easier. - CMake 3.1 or greater. - [mtxclient](https://github.com/Nheko-Reborn/mtxclient) - [LMDB](https://symas.com/lightning-memory-mapped-database/) From bbbd5df75f97271506e366325a378f1623881f3d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Sep 2019 01:33:46 +0200 Subject: [PATCH 13/94] Use system colors for now --- resources/qml/TimelineView.qml | 27 +++++++++++++++++++---- resources/qml/delegates/NoticeMessage.qml | 12 ++++++++++ resources/qml/delegates/TextMessage.qml | 1 + resources/res.qrc | 1 + src/AvatarProvider.cpp | 4 ---- 5 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 resources/qml/delegates/NoticeMessage.qml diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index f0f73ec9..e97b560a 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -1,17 +1,23 @@ import QtQuick 2.6 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.5 +import QtGraphicalEffects 1.0 import com.github.nheko 1.0 Rectangle { anchors.fill: parent + SystemPalette { id: colors; colorGroup: SystemPalette.Active } + SystemPalette { id: inactiveColors; colorGroup: SystemPalette.Disabled } + color: colors.window + Text { visible: !timelineManager.timeline anchors.centerIn: parent text: qsTr("No room open") font.pointSize: 24 + color: colors.windowText } ListView { @@ -67,16 +73,22 @@ Rectangle { Layout.alignment: Qt.AlignRight | Qt.AlignTop id: replyButton flat: true - height: replyButtonImg.contentHeight - width: replyButtonImg.contentWidth + height: 32 + width: 32 ToolTip.visible: hovered ToolTip.text: qsTr("Reply") + Image { id: replyButtonImg // Workaround, can't get icon.source working for now... anchors.fill: parent source: "qrc:/icons/icons/ui/mail-reply.png" } + ColorOverlay { + anchors.fill: replyButtonImg + source: replyButtonImg + color: colors.buttonText + } } Button { Layout.alignment: Qt.AlignRight | Qt.AlignTop @@ -92,6 +104,11 @@ Rectangle { anchors.fill: parent source: "qrc:/icons/icons/ui/vertical-ellipsis.png" } + ColorOverlay { + anchors.fill: optionsButtonImg + source: optionsButtonImg + color: colors.buttonText + } onClicked: contextMenu.open() @@ -117,6 +134,7 @@ Rectangle { Text { Layout.alignment: Qt.AlignRight | Qt.AlignTop text: model.timestamp.toLocaleTimeString("HH:mm") + color: inactiveColors.text } } @@ -134,13 +152,14 @@ Rectangle { anchors.horizontalCenter: parent.horizontalCenter visible: section.includes(" ") text: chat.model.formatDateSeparator(new Date(Number(section.split(" ")[1]))) + color: colors.windowText height: contentHeight * 1.2 width: contentWidth * 1.2 horizontalAlignment: Text.AlignHCenter background: Rectangle { radius: parent.height / 2 - color: "black" + color: colors.dark } } Row { @@ -155,7 +174,7 @@ Rectangle { Text { id: userName text: chat.model.displayName(section.split(" ")[0]) - color: chat.model.userColor(section.split(" ")[0], "#ffffff") + color: chat.model.userColor(section.split(" ")[0], colors.window) } } } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml new file mode 100644 index 00000000..5f04d235 --- /dev/null +++ b/resources/qml/delegates/NoticeMessage.qml @@ -0,0 +1,12 @@ +import QtQuick 2.5 + +TextEdit { + text: eventData.formattedBody + textFormat: TextEdit.RichText + readOnly: true + wrapMode: Text.Wrap + width: parent.width + selectByMouse: true + font.italic: true + color: inactiveColors.text +} diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 5f4b33fa..f7dba618 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -7,4 +7,5 @@ TextEdit { wrapMode: Text.Wrap width: parent.width selectByMouse: true + color: colors.text } diff --git a/resources/res.qrc b/resources/res.qrc index b2f27814..b18835fb 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -117,5 +117,6 @@ qml/TimelineView.qml qml/delegates/TextMessage.qml + qml/delegates/NoticeMessage.qml diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp index ec745c04..c83ffe0f 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp @@ -43,7 +43,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca QPixmap pixmap; if (avatar_cache.find(cacheKey, &pixmap)) { - nhlog::net()->info("cached pixmap {}", avatarUrl.toStdString()); callback(pixmap); return; } @@ -52,7 +51,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca if (!data.isNull()) { pixmap.loadFromData(data); avatar_cache.insert(cacheKey, pixmap); - nhlog::net()->info("loaded pixmap from disk cache {}", avatarUrl.toStdString()); callback(pixmap); return; } @@ -86,8 +84,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca cache::client()->saveImage(opts.mxc_url, res); - nhlog::net()->info("downloaded pixmap {}", opts.mxc_url); - emit proxy->avatarDownloaded(QByteArray(res.data(), res.size())); }); } From 8727831de7308ee5cf202c9b20a7bbf916405b2a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Sep 2019 02:01:44 +0200 Subject: [PATCH 14/94] Fix QML emojis --- resources/qml/TimelineView.qml | 3 ++- src/Utils.cpp | 5 ++--- src/timeline2/TimelineModel.cpp | 6 ++++++ src/timeline2/TimelineModel.h | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index e97b560a..76e728c3 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -173,8 +173,9 @@ Rectangle { Text { id: userName - text: chat.model.displayName(section.split(" ")[0]) + text: chat.model.escapeEmoji(chat.model.displayName(section.split(" ")[0])) color: chat.model.userColor(section.split(" ")[0], colors.window) + textFormat: Text.RichText } } } diff --git a/src/Utils.cpp b/src/Utils.cpp index 8c02b1c2..d458dbcc 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -40,9 +40,8 @@ utils::replaceEmoji(const QString &body) for (auto &code : utf32_string) { // TODO: Be more precise here. if (code > 9000) - fmtBody += - QString("") + - QString::fromUcs4(&code, 1) + ""; + fmtBody += QString("") + + QString::fromUcs4(&code, 1) + ""; else fmtBody += QString::fromUcs4(&code, 1); } diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index c1918d20..dff5e56e 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -336,3 +336,9 @@ TimelineModel::formatDateSeparator(QDate date) const return date.toString(fmt); } + +QString +TimelineModel::escapeEmoji(QString str) const +{ + return utils::replaceEmoji(str); +} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 51cb9be3..e37c6542 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -91,6 +91,7 @@ public: Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString displayName(QString id) const; Q_INVOKABLE QString formatDateSeparator(QDate date) const; + Q_INVOKABLE QString escapeEmoji(QString str) const; void addEvents(const mtx::responses::Timeline &events); From 7947ba57cc941c7af1f9b59796180df9b2fc60a0 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Sep 2019 12:35:44 +0200 Subject: [PATCH 15/94] Make reply and options buttons smaller --- resources/qml/TimelineView.qml | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 76e728c3..5f068e57 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -73,11 +73,14 @@ Rectangle { Layout.alignment: Qt.AlignRight | Qt.AlignTop id: replyButton flat: true - height: 32 - width: 32 + Layout.preferredHeight: 16 ToolTip.visible: hovered ToolTip.text: qsTr("Reply") + // disable background, because we don't want a border on hover + background: Item { + } + Image { id: replyButtonImg // Workaround, can't get icon.source working for now... @@ -87,17 +90,21 @@ Rectangle { ColorOverlay { anchors.fill: replyButtonImg source: replyButtonImg - color: colors.buttonText + color: replyButton.hovered ? colors.highlight : colors.buttonText } } Button { Layout.alignment: Qt.AlignRight | Qt.AlignTop id: optionsButton flat: true - height: optionsButtonImg.contentHeight - width: optionsButtonImg.contentWidth + Layout.preferredHeight: 16 ToolTip.visible: hovered ToolTip.text: qsTr("Options") + + // disable background, because we don't want a border on hover + background: Item { + } + Image { id: optionsButtonImg // Workaround, can't get icon.source working for now... @@ -107,7 +114,7 @@ Rectangle { ColorOverlay { anchors.fill: optionsButtonImg source: optionsButtonImg - color: colors.buttonText + color: optionsButton.hovered ? colors.highlight : colors.buttonText } onClicked: contextMenu.open() @@ -135,6 +142,15 @@ Rectangle { Layout.alignment: Qt.AlignRight | Qt.AlignTop text: model.timestamp.toLocaleTimeString("HH:mm") color: inactiveColors.text + + ToolTip.visible: ma.containsMouse + ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) + + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } } } From aae295cb02920d00dd6f31b82f9f267aa10f42de Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Sep 2019 14:37:54 +0200 Subject: [PATCH 16/94] Fix new messages not arriving in qml timeline --- src/timeline2/TimelineModel.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index dff5e56e..28820205 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -241,6 +241,9 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) isInitialSync = false; } + if (timeline.events.empty()) + return; + std::vector ids; for (const auto &e : timeline.events) { QString id = @@ -251,8 +254,8 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) } beginInsertRows(QModelIndex(), - static_cast(this->events.size()), - static_cast(this->events.size() + ids.size() - 1)); + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size() + ids.size() - 1)); this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); endInsertRows(); } From ebeb1eb7721f357b016f6e914509918b6bee5356 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Sep 2019 22:22:07 +0200 Subject: [PATCH 17/94] Implement avatars in qml timeline --- CMakeLists.txt | 1 + resources/qml/Avatar.qml | 45 +++++++++++++++ resources/qml/TimelineView.qml | 5 +- resources/res.qrc | 1 + src/MxcImageProvider.cpp | 79 +++++++++++++++++++++++++++ src/MxcImageProvider.h | 48 ++++++++++++++++ src/RoomInfoListItem.cpp | 2 +- src/UserSettingsPage.cpp | 6 +- src/timeline2/TimelineModel.cpp | 6 ++ src/timeline2/TimelineModel.h | 1 + src/timeline2/TimelineViewManager.cpp | 2 + src/ui/Avatar.cpp | 2 +- 12 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 resources/qml/Avatar.qml create mode 100644 src/MxcImageProvider.cpp create mode 100644 src/MxcImageProvider.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8013fed9..d386efbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -231,6 +231,7 @@ set(SRC_FILES src/Logging.cpp src/MainWindow.cpp src/MatrixClient.cpp + src/MxcImageProvider.cpp src/QuickSwitcher.cpp src/Olm.cpp src/RegisterPage.cpp diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml new file mode 100644 index 00000000..9d7b54fe --- /dev/null +++ b/resources/qml/Avatar.qml @@ -0,0 +1,45 @@ +import QtQuick 2.6 +import QtGraphicalEffects 1.0 +import Qt.labs.settings 1.0 + +Rectangle { + id: avatar + width: 48 + height: 48 + radius: settings.avatar_circles ? height/2 : 3 + + Settings { + id: settings + category: "user" + property bool avatar_circles: true + } + + property alias url: img.source + property string displayName + + Text { + anchors.fill: parent + text: String.fromCodePoint(displayName.codePointAt(0)) + color: colors.text + font.pixelSize: avatar.height/2 + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + Image { + id: img + anchors.fill: parent + asynchronous: true + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + anchors.fill: parent + width: avatar.width + height: avatar.height + radius: settings.avatar_circles ? height/2 : 3 + } + } + } + color: colors.dark +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 5f068e57..0151686a 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -181,10 +181,11 @@ Rectangle { Row { height: userName.height spacing: 4 - Rectangle { + Avatar { width: 48 height: 48 - color: "green" + url: chat.model.avatarUrl(section.split(" ")[0]).replace("mxc://", "image://MxcImage/") + displayName: chat.model.displayName(section.split(" ")[0]) } Text { diff --git a/resources/res.qrc b/resources/res.qrc index b18835fb..6f6d480a 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -116,6 +116,7 @@ qml/TimelineView.qml + qml/Avatar.qml qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp new file mode 100644 index 00000000..305439fc --- /dev/null +++ b/src/MxcImageProvider.cpp @@ -0,0 +1,79 @@ +#include "MxcImageProvider.h" + +#include "Cache.h" + +void +MxcImageResponse::run() +{ + if (m_requestedSize.isValid()) { + QString fileName = QString("%1_%2x%3") + .arg(m_id) + .arg(m_requestedSize.width()) + .arg(m_requestedSize.height()); + + auto data = cache::client()->image(fileName); + if (!data.isNull() && m_image.loadFromData(data)) { + m_image = m_image.scaled(m_requestedSize, Qt::KeepAspectRatio); + m_image.setText("mxc url", "mxc://" + m_id); + emit finished(); + return; + } + + mtx::http::ThumbOpts opts; + opts.mxc_url = "mxc://" + m_id.toStdString(); + opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1; + opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1; + opts.method = "scale"; + http::client()->get_thumbnail( + opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download image {}", + m_id.toStdString()); + m_error = "Failed download"; + emit finished(); + + return; + } + + auto data = QByteArray(res.data(), res.size()); + cache::client()->saveImage(fileName, data); + m_image.loadFromData(data); + m_image = m_image.scaled(m_requestedSize, Qt::KeepAspectRatio); + m_image.setText("mxc url", "mxc://" + m_id); + + emit finished(); + }); + } else { + auto data = cache::client()->image(m_id); + if (!data.isNull() && m_image.loadFromData(data)) { + m_image.setText("mxc url", "mxc://" + m_id); + emit finished(); + return; + } + + http::client()->download( + "mxc://" + m_id.toStdString(), + [this](const std::string &res, + const std::string &, + const std::string &originalFilename, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download image {}", + m_id.toStdString()); + m_error = "Failed download"; + emit finished(); + + return; + } + + auto data = QByteArray(res.data(), res.size()); + m_image.loadFromData(data); + m_image.setText("original filename", + QString::fromStdString(originalFilename)); + m_image.setText("mxc url", "mxc://" + m_id); + cache::client()->saveImage(m_id, data); + + emit finished(); + }); + } +} diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h new file mode 100644 index 00000000..8710171c --- /dev/null +++ b/src/MxcImageProvider.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include +#include + +class MxcImageResponse + : public QQuickImageResponse + , public QRunnable +{ +public: + MxcImageResponse(const QString &id, const QSize &requestedSize) + : m_id(id) + , m_requestedSize(requestedSize) + { + setAutoDelete(false); + } + + QQuickTextureFactory *textureFactory() const override + { + return QQuickTextureFactory::textureFactoryForImage(m_image); + } + QString errorString() const override { return m_error; } + + void run() override; + + QString m_id, m_error; + QSize m_requestedSize; + QImage m_image; +}; + +class MxcImageProvider : public QQuickAsyncImageProvider +{ +public: + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override + { + MxcImageResponse *response = new MxcImageResponse(id, requestedSize); + pool.start(response); + return response; + } + +private: + QThreadPool pool; +}; + diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index 8aadbea2..f135451c 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -142,7 +142,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *) void RoomInfoListItem::paintEvent(QPaintEvent *event) { - bool rounded = QSettings().value("user/avatar/circles", true).toBool(); + bool rounded = QSettings().value("user/avatar_circles", true).toBool(); Q_UNUSED(event); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 9fd033e9..1caea449 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -53,7 +53,7 @@ UserSettings::load() isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); theme_ = settings.value("user/theme", defaultTheme_).toString(); font_ = settings.value("user/font_family", "default").toString(); - avatarCircles_ = settings.value("user/avatar/circles", true).toBool(); + avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); @@ -119,9 +119,7 @@ UserSettings::save() settings.setValue("start_in_tray", isStartInTrayEnabled_); settings.endGroup(); - settings.beginGroup("avatar"); - settings.setValue("circles", avatarCircles_); - settings.endGroup(); + settings.setValue("avatar_circles", avatarCircles_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", isTypingNotificationsEnabled_); diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 28820205..310494b4 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -325,6 +325,12 @@ TimelineModel::displayName(QString id) const return Cache::displayName(room_id_, id); } +QString +TimelineModel::avatarUrl(QString id) const +{ + return Cache::avatarUrl(room_id_, id); +} + QString TimelineModel::formatDateSeparator(QDate date) const { diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index e37c6542..954da5eb 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -90,6 +90,7 @@ public: 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; diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 0e0e74e4..eb9bea54 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -4,6 +4,7 @@ #include #include "Logging.h" +#include "MxcImageProvider.h" TimelineViewManager::TimelineViewManager(QWidget *parent) { @@ -18,6 +19,7 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) container = QWidget::createWindowContainer(view, parent); container->setMinimumSize(200, 200); view->rootContext()->setContextProperty("timelineManager", this); + view->engine()->addImageProvider("MxcImage", new MxcImageProvider()); view->setSource(QUrl("qrc:///qml/TimelineView.qml")); } diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp index 501a8968..e4a90f81 100644 --- a/src/ui/Avatar.cpp +++ b/src/ui/Avatar.cpp @@ -101,7 +101,7 @@ Avatar::setIcon(const QIcon &icon) void Avatar::paintEvent(QPaintEvent *) { - bool rounded = QSettings().value("user/avatar/circles", true).toBool(); + bool rounded = QSettings().value("user/avatar_circles", true).toBool(); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); From 86f4119a0502ffefd60abd5963f0d52628ba4e78 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Sep 2019 12:44:46 +0200 Subject: [PATCH 18/94] Implement basic ImageMessages in qml timeline I suck at sizing so the images in the message are currently hardcoded to 300 pixels in width... --- resources/qml/TimelineView.qml | 6 ++- resources/qml/delegates/ImageMessage.qml | 14 +++++ resources/res.qrc | 1 + src/MxcImageProvider.h | 1 - src/timeline2/TimelineModel.cpp | 66 ++++++++++++++++++++++++ src/timeline2/TimelineModel.h | 4 ++ 6 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 resources/qml/delegates/ImageMessage.qml diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 0151686a..399e85eb 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -23,6 +23,8 @@ Rectangle { ListView { id: chat + cacheBuffer: 4*parent.height + visible: timelineManager.timeline != null anchors.fill: parent @@ -40,12 +42,14 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: scrollbar.width + height: loader.height Loader { id: loader + asynchronous: false Layout.fillWidth: true - height: item.height Layout.alignment: Qt.AlignTop + height: item.height source: switch(model.type) { case MtxEvent.Aliases: return "delegates/Aliases.qml" diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml new file mode 100644 index 00000000..a3bc78e5 --- /dev/null +++ b/resources/qml/delegates/ImageMessage.qml @@ -0,0 +1,14 @@ +import QtQuick 2.6 + +Item { + width: 300 + height: 300 * eventData.proportionalHeight + + Image { + anchors.fill: parent + + source: eventData.url.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit + } +} diff --git a/resources/res.qrc b/resources/res.qrc index 6f6d480a..62ed53e5 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -119,5 +119,6 @@ qml/Avatar.qml qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml + qml/delegates/ImageMessage.qml diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index 8710171c..19d8a74e 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -45,4 +45,3 @@ public: private: QThreadPool pool; }; - diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 310494b4..16f1dfe6 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -58,6 +58,20 @@ eventFormattedBody(const mtx::events::RoomEvent &e) return QString::fromStdString(e.content.body); } +template +QString +eventUrl(const T &) +{ + return ""; +} +template +auto +eventUrl(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.url); +} + template qml_mtx_events::EventType toRoomEventType(const mtx::events::Event &e) @@ -146,6 +160,41 @@ toRoomEventType(const mtx::events::Event &) } // ::EventType::Type toRoomEventType(const Event &e) { return // ::EventType::LocationMessage; } + +template +uint64_t +eventHeight(const mtx::events::Event &) +{ + return -1; +} +template +auto +eventHeight(const mtx::events::RoomEvent &e) -> decltype(e.content.info.h) +{ + return e.content.info.h; +} +template +uint64_t +eventWidth(const mtx::events::Event &) +{ + return -1; +} +template +auto +eventWidth(const mtx::events::RoomEvent &e) -> decltype(e.content.info.w) +{ + return e.content.info.w; +} + +template +double +eventPropHeight(const mtx::events::RoomEvent &e) +{ + auto w = eventWidth(e); + if (w == 0) + w = 1; + return eventHeight(e) / (double)w; +} } TimelineModel::TimelineModel(QString room_id, QObject *parent) @@ -167,6 +216,10 @@ TimelineModel::roleNames() const {UserId, "userId"}, {UserName, "userName"}, {Timestamp, "timestamp"}, + {Url, "url"}, + {Height, "height"}, + {Width, "width"}, + {ProportionalHeight, "proportionalHeight"}, }; } int @@ -228,6 +281,19 @@ TimelineModel::data(const QModelIndex &index, int role) const return QVariant(utils::replaceEmoji(boost::apply_visitor( [](const auto &e) -> QString { return eventFormattedBody(e); }, events.value(id)))); + case Url: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventUrl(e); }, events.value(id))); + case Height: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventHeight(e); }, events.value(id))); + case Width: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventWidth(e); }, events.value(id))); + case ProportionalHeight: + return QVariant(boost::apply_visitor( + [](const auto &e) -> double { return eventPropHeight(e); }, events.value(id))); + default: return QVariant(); } diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 954da5eb..66d03cf5 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -82,6 +82,10 @@ public: UserId, UserName, Timestamp, + Url, + Height, + Width, + ProportionalHeight, }; QHash roleNames() const override; From 7aca8a94304b47904f214325b969c115944296c8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Sep 2019 15:26:46 +0200 Subject: [PATCH 19/94] Reenable view raw message --- resources/qml/TimelineView.qml | 3 +-- src/timeline2/TimelineModel.cpp | 13 ++++++++++++- src/timeline2/TimelineModel.h | 2 ++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 399e85eb..36701c72 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -42,11 +42,9 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: scrollbar.width - height: loader.height Loader { id: loader - asynchronous: false Layout.fillWidth: true Layout.alignment: Qt.AlignTop height: item.height @@ -135,6 +133,7 @@ Rectangle { } MenuItem { text: "View raw message" + onTriggered: chat.model.viewRawMessage(model.id) } MenuItem { text: "Redact message" diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 16f1dfe6..5fd54170 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -6,6 +6,7 @@ #include "Logging.h" #include "Utils.h" +#include "dialogs/RawMessage.h" namespace { template @@ -220,6 +221,7 @@ TimelineModel::roleNames() const {Height, "height"}, {Width, "width"}, {ProportionalHeight, "proportionalHeight"}, + {Id, "id"}, }; } int @@ -293,7 +295,8 @@ TimelineModel::data(const QModelIndex &index, int role) const case ProportionalHeight: return QVariant(boost::apply_visitor( [](const auto &e) -> double { return eventPropHeight(e); }, events.value(id))); - + case Id: + return id; default: return QVariant(); } @@ -417,3 +420,11 @@ TimelineModel::escapeEmoji(QString str) const { return utils::replaceEmoji(str); } + +void +TimelineModel::viewRawMessage(QString id) const +{ + std::string ev = utils::serialize_event(events.value(id)).dump(4); + auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); + Q_UNUSED(dialog); +} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 66d03cf5..02a0c168 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -86,6 +86,7 @@ public: Height, Width, ProportionalHeight, + Id, }; QHash roleNames() const override; @@ -97,6 +98,7 @@ public: 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; void addEvents(const mtx::responses::Timeline &events); From e20501cec791d396ae03bc63958a36756607e711 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Sep 2019 16:50:32 +0200 Subject: [PATCH 20/94] Reenable display of encrypted messages --- src/timeline2/TimelineModel.cpp | 125 ++++++++++++++++++++++++++++---- src/timeline2/TimelineModel.h | 15 +++- 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 5fd54170..0d4ec239 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -5,6 +5,7 @@ #include #include "Logging.h" +#include "Olm.h" #include "Utils.h" #include "dialogs/RawMessage.h" @@ -239,14 +240,20 @@ TimelineModel::data(const QModelIndex &index, int role) const QString id = eventOrder[index.row()]; + mtx::events::collections::TimelineEvents event = events.value(id); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + switch (role) { case Section: { QDateTime date = boost::apply_visitor( - [](const auto &e) -> QDateTime { return eventTimestamp(e); }, events.value(id)); + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event); date.setTime(QTime()); - QString userId = boost::apply_visitor( - [](const auto &e) -> QString { return senderId(e); }, events.value(id)); + QString userId = + boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event); for (int r = index.row() - 1; r > 0; r--) { QDateTime prevDate = boost::apply_visitor( @@ -267,34 +274,33 @@ TimelineModel::data(const QModelIndex &index, int role) const } case UserId: return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return senderId(e); }, events.value(id))); + [](const auto &e) -> QString { return senderId(e); }, event)); case UserName: return QVariant(displayName(boost::apply_visitor( - [](const auto &e) -> QString { return senderId(e); }, events.value(id)))); + [](const auto &e) -> QString { return senderId(e); }, event))); case Timestamp: return QVariant(boost::apply_visitor( - [](const auto &e) -> QDateTime { return eventTimestamp(e); }, events.value(id))); + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event)); case Type: return QVariant(boost::apply_visitor( [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, - events.value(id))); + event)); case FormattedBody: return QVariant(utils::replaceEmoji(boost::apply_visitor( - [](const auto &e) -> QString { return eventFormattedBody(e); }, - events.value(id)))); + [](const auto &e) -> QString { return eventFormattedBody(e); }, event))); case Url: return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return eventUrl(e); }, events.value(id))); + [](const auto &e) -> QString { return eventUrl(e); }, event)); case Height: return QVariant(boost::apply_visitor( - [](const auto &e) -> qulonglong { return eventHeight(e); }, events.value(id))); + [](const auto &e) -> qulonglong { return eventHeight(e); }, event)); case Width: return QVariant(boost::apply_visitor( - [](const auto &e) -> qulonglong { return eventWidth(e); }, events.value(id))); + [](const auto &e) -> qulonglong { return eventWidth(e); }, event)); case ProportionalHeight: return QVariant(boost::apply_visitor( - [](const auto &e) -> double { return eventPropHeight(e); }, events.value(id))); + [](const auto &e) -> double { return eventPropHeight(e); }, event)); case Id: return id; default: @@ -428,3 +434,96 @@ TimelineModel::viewRawMessage(QString id) const auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); Q_UNUSED(dialog); } + +DecryptionResult +TimelineModel::decryptEvent(const mtx::events::EncryptedEvent &e) const +{ + MegolmSessionIndex index; + index.room_id = room_id_.toStdString(); + index.session_id = e.content.session_id; + index.sender_key = e.content.sender_key; + + mtx::events::RoomEvent dummy; + dummy.origin_server_ts = e.origin_server_ts; + dummy.event_id = e.event_id; + dummy.sender = e.sender; + dummy.content.body = + tr("-- Encrypted Event (No keys found for decryption) --", + "Placeholder, when the message was not decrypted yet or can't be decrypted") + .toStdString(); + + try { + if (!cache::client()->inboundMegolmSessionExists(index)) { + nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); + // TODO: request megolm session_id & session_key from the sender. + return {dummy, false}; + } + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); + dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", + "Placeholder, when the message can't be decrypted, because " + "the DB access failed when trying to lookup the session.") + .toStdString(); + return {dummy, false}; + } + + std::string msg_str; + try { + auto session = cache::client()->getInboundMegolmSession(index); + auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (failed to retrieve megolm keys from db) --", + "Placeholder, when the message can't be decrypted, because the DB access " + "failed.") + .toStdString(); + return {dummy, false}; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (%1) --", + "Placeholder, when the message can't be decrypted. In this case, the Olm " + "decrytion returned an error, which is passed ad %1") + .arg(e.what()) + .toStdString(); + return {dummy, false}; + } + + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = e.event_id; + body["sender"] = e.sender; + body["origin_server_ts"] = e.origin_server_ts; + body["unsigned"] = e.unsigned_data; + + nhlog::crypto()->debug("decrypted event: {}", e.event_id); + + json event_array = json::array(); + event_array.push_back(body); + + std::vector events; + mtx::responses::utils::parse_timeline_events(event_array, events); + + if (events.size() == 1) + return {events.at(0), true}; + + dummy.content.body = + tr("-- 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") + .toStdString(); + return {dummy, false}; +} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 02a0c168..d63ed818 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -1,12 +1,12 @@ #pragma once +#include + #include #include #include #include -#include - namespace qml_mtx_events { Q_NAMESPACE @@ -66,6 +66,14 @@ enum EventType Q_ENUM_NS(EventType) } +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 TimelineModel : public QAbstractListModel { Q_OBJECT @@ -114,6 +122,9 @@ signals: void oldMessagesRetrieved(const mtx::responses::Messages &res); private: + DecryptionResult decryptEvent( + const mtx::events::EncryptedEvent &e) const; + QHash events; std::vector eventOrder; From 656861fa9d71807ab6fa766ac115cd23853c6004 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Sep 2019 17:28:26 +0200 Subject: [PATCH 21/94] Fix ci formatter to format all files --- .ci/format.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.ci/format.sh b/.ci/format.sh index d3b629c3..e1e6c1e4 100755 --- a/.ci/format.sh +++ b/.ci/format.sh @@ -11,5 +11,7 @@ FILES=$(find src -type f -type f \( -iname "*.cpp" -o -iname "*.h" \)) for f in $FILES do - clang-format -i "$f" && git diff --exit-code + clang-format -i "$f" done; + +git diff --exit-code From f260b8b4aede752d66e4351fe4c09282d7b93db7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Sep 2019 17:28:40 +0200 Subject: [PATCH 22/94] Fix shadow error --- src/timeline2/TimelineModel.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 0d4ec239..d3d6f5c7 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -514,11 +514,11 @@ TimelineModel::decryptEvent(const mtx::events::EncryptedEvent events; - mtx::responses::utils::parse_timeline_events(event_array, events); + std::vector temp_events; + mtx::responses::utils::parse_timeline_events(event_array, temp_events); - if (events.size() == 1) - return {events.at(0), true}; + if (temp_events.size() == 1) + return {temp_events.at(0), true}; dummy.content.body = tr("-- Encrypted Event (Unknown event type) --", From 4efac5a247721c1c5ba4c072be0bce21ee620039 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Sep 2019 20:33:22 +0200 Subject: [PATCH 23/94] Try to fix duplicate messages in certain edge cases (i.e. sync and pagination at the same time) --- src/timeline2/TimelineModel.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index d3d6f5c7..54f03b56 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -324,10 +324,16 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) QString id = boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); + if (this->events.contains(id)) + continue; + this->events.insert(id, e); ids.push_back(id); } + if (ids.empty) + return; + beginInsertRows(QModelIndex(), static_cast(this->eventOrder.size()), static_cast(this->eventOrder.size() + ids.size() - 1)); @@ -372,13 +378,18 @@ TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) QString id = boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); + if (this->events.contains(id)) + continue; + this->events.insert(id, e); ids.push_back(id); } - beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); - this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); - endInsertRows(); + if (!ids.empty()) { + beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); + endInsertRows(); + } prev_batch_token_ = QString::fromStdString(msgs.end); From a1c97fc8d6e6f835aab79e2f8e37ce8488bcb5b6 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Sep 2019 21:42:33 +0200 Subject: [PATCH 24/94] Show redactions in qml timeline --- resources/qml/TimelineView.qml | 3 +- resources/qml/delegates/Redacted.qml | 15 ++++++ resources/res.qrc | 1 + src/timeline2/TimelineModel.cpp | 79 ++++++++++++++++++++-------- src/timeline2/TimelineModel.h | 3 ++ 5 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 resources/qml/delegates/Redacted.qml diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 36701c72..5c96ff18 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -23,7 +23,7 @@ Rectangle { ListView { id: chat - cacheBuffer: 4*parent.height + cacheBuffer: parent.height visible: timelineManager.timeline != null anchors.fill: parent @@ -65,6 +65,7 @@ Rectangle { case MtxEvent.TextMessage: return "delegates/TextMessage.qml" case MtxEvent.ImageMessage: return "delegates/ImageMessage.qml" case MtxEvent.VideoMessage: return "delegates/VideoMessage.qml" + case MtxEvent.Redacted: return "delegates/Redacted.qml" default: return "delegates/placeholder.qml" } property variant eventData: model diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml new file mode 100644 index 00000000..53e95a83 --- /dev/null +++ b/resources/qml/delegates/Redacted.qml @@ -0,0 +1,15 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.5 + +Label { + text: qsTr("redacted") + color: inactiveColors.text + horizontalAlignment: Text.AlignHCenter + + height: contentHeight * 1.2 + width: contentWidth * 1.2 + background: Rectangle { + radius: parent.height / 2 + color: colors.dark + } +} diff --git a/resources/res.qrc b/resources/res.qrc index 62ed53e5..0d55e70d 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -120,5 +120,6 @@ qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml + qml/delegates/Redacted.qml diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 54f03b56..f544c83c 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -1,5 +1,6 @@ #include "TimelineModel.h" +#include #include #include @@ -160,6 +161,12 @@ toRoomEventType(const mtx::events::Event &) { return qml_mtx_events::EventType::VideoMessage; } + +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::Redacted; +} // ::EventType::Type toRoomEventType(const Event &e) { return // ::EventType::LocationMessage; } @@ -319,19 +326,9 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) if (timeline.events.empty()) return; - std::vector ids; - for (const auto &e : timeline.events) { - QString id = - boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); + std::vector ids = internalAddEvents(timeline.events); - if (this->events.contains(id)) - continue; - - this->events.insert(id, e); - ids.push_back(id); - } - - if (ids.empty) + if (ids.empty()) return; beginInsertRows(QModelIndex(), @@ -341,6 +338,52 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) endInsertRows(); } +std::vector +TimelineModel::internalAddEvents( + const std::vector &timeline) +{ + std::vector ids; + for (const auto &e : timeline) { + QString id = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); + + if (this->events.contains(id)) + continue; + + if (auto redaction = + boost::get>(&e)) { + QString redacts = QString::fromStdString(redaction->redacts); + auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts); + + if (redacted != eventOrder.end()) { + auto redactedEvent = boost::apply_visitor( + [](const auto &ev) + -> mtx::events::RoomEvent { + mtx::events::RoomEvent + replacement = {}; + replacement.event_id = ev.event_id; + replacement.room_id = ev.room_id; + replacement.sender = ev.sender; + replacement.origin_server_ts = ev.origin_server_ts; + replacement.type = ev.type; + return replacement; + }, + e); + events.insert(redacts, redactedEvent); + + int row = (int)std::distance(eventOrder.begin(), redacted); + emit dataChanged(index(row, 0), index(row, 0)); + } + + continue; // don't insert redaction into timeline + } + + this->events.insert(id, e); + ids.push_back(id); + } + return ids; +} + void TimelineModel::fetchHistory() { @@ -373,17 +416,7 @@ TimelineModel::fetchHistory() void TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) { - std::vector ids; - for (const auto &e : msgs.chunk) { - QString id = - boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); - - if (this->events.contains(id)) - continue; - - this->events.insert(id, e); - ids.push_back(id); - } + std::vector ids = internalAddEvents(msgs.chunk); if (!ids.empty()) { beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index d63ed818..ca8d4ad6 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -61,6 +61,7 @@ enum EventType NoticeMessage, TextMessage, VideoMessage, + Redacted, UnknownMessage, }; Q_ENUM_NS(EventType) @@ -124,6 +125,8 @@ signals: private: DecryptionResult decryptEvent( const mtx::events::EncryptedEvent &e) const; + std::vector internalAddEvents( + const std::vector &timeline); QHash events; std::vector eventOrder; From a7595eab5a6fdcde80b9989f65de1801afad3d3d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 11 Sep 2019 00:00:04 +0200 Subject: [PATCH 25/94] Reimplement sending basic text messages --- src/timeline2/TimelineModel.h | 29 ++++++++++++++++++++ src/timeline2/TimelineViewManager.cpp | 38 +++++++++++++++++++++++++++ src/timeline2/TimelineViewManager.h | 4 +-- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index ca8d4ad6..59321119 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -7,6 +7,9 @@ #include #include +#include "Logging.h" +#include "MatrixClient.h" + namespace qml_mtx_events { Q_NAMESPACE @@ -110,6 +113,8 @@ public: Q_INVOKABLE void viewRawMessage(QString id) const; void addEvents(const mtx::responses::Timeline &events); + template + void sendMessage(const T &msg); public slots: void fetchHistory(); @@ -121,6 +126,8 @@ private slots: signals: void oldMessagesRetrieved(const mtx::responses::Messages &res); + void messageFailed(const std::string txn_id); + void messageSent(const std::string txn_id, std::string event_id); private: DecryptionResult decryptEvent( @@ -139,3 +146,25 @@ private: QHash userColors; }; + +template +void +TimelineModel::sendMessage(const T &msg) +{ + auto txn_id = http::client()->generate_txn_id(); + http::client()->send_room_message( + room_id_.toStdString(), + txn_id, + msg, + [this, txn_id](const mtx::responses::EventId &res, mtx::http::RequestErr err) { + if (err) { + const int status_code = static_cast(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(txn_id); + } + emit messageSent(txn_id, res.event_id.to_string()); + }); +} diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index eb9bea54..6aa2ff43 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -62,3 +62,41 @@ TimelineViewManager::initWithMessages(const std::mapaddEvents(e.second); } } + +void +TimelineViewManager::queueTextMessage(const QString &msg) +{ + mtx::events::msg::Text text = {}; + text.body = msg.trimmed().toStdString(); + text.format = "org.matrix.custom.html"; + text.formatted_body = utils::markdownToHtml(msg).toStdString(); + + if (timeline_) + timeline_->sendMessage(text); +} + +void +TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) +{ + mtx::events::msg::Text text = {}; + + QString body; + bool firstLine = true; + for (const auto &line : related.quoted_body.splitRef("\n")) { + if (firstLine) { + firstLine = false; + body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); + } else { + body = QString("%1\n> %2\n").arg(body).arg(line); + } + } + + text.body = QString("%1\n%2").arg(body).arg(reply).toStdString(); + text.format = "org.matrix.custom.html"; + text.formatted_body = + utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString(); + text.relates_to.in_reply_to.event_id = related.related_event; + + if (timeline_) + timeline_->sendMessage(text); +} diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 1bec8746..7ec0da5f 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -46,8 +46,8 @@ public slots: void setHistoryView(const QString &room_id); - void queueTextMessage(const QString &msg) {} - void queueReplyMessage(const QString &reply, const RelatedInfo &related) {} + void queueTextMessage(const QString &msg); + void queueReplyMessage(const QString &reply, const RelatedInfo &related); void queueEmoteMessage(const QString &msg) {} void queueImageMessage(const QString &roomid, const QString &filename, From 5c87d6faa60b14e4f84c67b2839615cbcb927f9f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 11 Sep 2019 00:17:45 +0200 Subject: [PATCH 26/94] Implement sending other message types in qml timeline not using placeholders in timeline for now --- src/timeline2/TimelineViewManager.cpp | 78 +++++++++++++++++++++++++++ src/timeline2/TimelineViewManager.h | 14 ++--- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 6aa2ff43..b0ed4eab 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -100,3 +100,81 @@ TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo & if (timeline_) timeline_->sendMessage(text); } + +void +TimelineViewManager::queueEmoteMessage(const QString &msg) +{ + auto html = utils::markdownToHtml(msg); + + mtx::events::msg::Emote emote; + emote.body = msg.trimmed().toStdString(); + + if (html != msg.trimmed().toHtmlEscaped()) + emote.formatted_body = html.toStdString(); + + if (timeline_) + timeline_->sendMessage(emote); +} + +void +TimelineViewManager::queueImageMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions) +{ + mtx::events::msg::Image image; + image.info.mimetype = mime.toStdString(); + image.info.size = dsize; + image.body = filename.toStdString(); + image.url = url.toStdString(); + image.info.h = dimensions.height(); + image.info.w = dimensions.width(); + models.value(roomid)->sendMessage(image); +} + +void +TimelineViewManager::queueFileMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) +{ + mtx::events::msg::File file; + file.info.mimetype = mime.toStdString(); + file.info.size = dsize; + file.body = filename.toStdString(); + file.url = url.toStdString(); + models.value(roomid)->sendMessage(file); +} + +void +TimelineViewManager::queueAudioMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) +{ + mtx::events::msg::Audio audio; + audio.info.mimetype = mime.toStdString(); + audio.info.size = dsize; + audio.body = filename.toStdString(); + audio.url = url.toStdString(); + models.value(roomid)->sendMessage(audio); +} + +void +TimelineViewManager::queueVideoMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) +{ + mtx::events::msg::Video video; + video.info.mimetype = mime.toStdString(); + video.info.size = dsize; + video.body = filename.toStdString(); + video.url = url.toStdString(); + models.value(roomid)->sendMessage(video); +} diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 7ec0da5f..c2d1a8c0 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -48,32 +48,28 @@ public slots: void queueTextMessage(const QString &msg); void queueReplyMessage(const QString &reply, const RelatedInfo &related); - void queueEmoteMessage(const QString &msg) {} + void queueEmoteMessage(const QString &msg); void queueImageMessage(const QString &roomid, const QString &filename, const QString &url, const QString &mime, uint64_t dsize, - const QSize &dimensions) - {} + const QSize &dimensions); void queueFileMessage(const QString &roomid, const QString &filename, const QString &url, const QString &mime, - uint64_t dsize) - {} + uint64_t dsize); void queueAudioMessage(const QString &roomid, const QString &filename, const QString &url, const QString &mime, - uint64_t dsize) - {} + uint64_t dsize); void queueVideoMessage(const QString &roomid, const QString &filename, const QString &url, const QString &mime, - uint64_t dsize) - {} + uint64_t dsize); private: QQuickView *view; From 62d0cd74da856028147ce222f3af9ad940b70a1b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 11 Sep 2019 00:54:40 +0200 Subject: [PATCH 27/94] Implement replies in qml timeline --- resources/qml/TimelineView.qml | 2 ++ src/timeline2/TimelineModel.cpp | 54 +++++++++++++++++++++++++++++++++ src/timeline2/TimelineModel.h | 1 + 3 files changed, 57 insertions(+) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 5c96ff18..4f10f352 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -95,6 +95,8 @@ Rectangle { source: replyButtonImg color: replyButton.hovered ? colors.highlight : colors.buttonText } + + onClicked: chat.model.replyAction(model.id) } Button { Layout.alignment: Qt.AlignRight | Qt.AlignTop diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index f544c83c..46a33add 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -5,6 +5,7 @@ #include +#include "ChatPage.h" #include "Logging.h" #include "Olm.h" #include "Utils.h" @@ -37,6 +38,33 @@ eventTimestamp(const T &event) return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); } +template +std::string +eventMsgType(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventMsgType(const mtx::events::RoomEvent &e) -> decltype(e.content.msgtype) +{ + return e.content.msgtype; +} + +template +QString +eventBody(const mtx::events::Event &) +{ + return QString(""); +} +template +auto +eventBody(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.body); +} + template QString eventFormattedBody(const mtx::events::Event &) @@ -293,6 +321,9 @@ TimelineModel::data(const QModelIndex &index, int role) const return QVariant(boost::apply_visitor( [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, event)); + case Body: + return QVariant(utils::replaceEmoji(boost::apply_visitor( + [](const auto &e) -> QString { return eventBody(e); }, event))); case FormattedBody: return QVariant(utils::replaceEmoji(boost::apply_visitor( [](const auto &e) -> QString { return eventFormattedBody(e); }, event))); @@ -571,3 +602,26 @@ TimelineModel::decryptEvent(const mtx::events::EncryptedEvent RelatedInfo { + RelatedInfo related_ = {}; + related_.quoted_user = QString::fromStdString(ev.sender); + related_.related_event = ev.event_id; + return related_; + }, + event); + related.type = mtx::events::getMessageType(boost::apply_visitor( + [](const auto &e) -> std::string { return eventMsgType(e); }, event)); + related.quoted_body = + boost::apply_visitor([](const auto &e) -> QString { return eventBody(e); }, event); + + if (related.quoted_body.isEmpty()) + return; + + emit ChatPage::instance()->messageReply(related); +} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 59321119..b2481668 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -111,6 +111,7 @@ public: Q_INVOKABLE QString formatDateSeparator(QDate date) const; Q_INVOKABLE QString escapeEmoji(QString str) const; Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void replyAction(QString id); void addEvents(const mtx::responses::Timeline &events); template From 691c8542019284044e50ef14e166930993953be8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 11 Sep 2019 00:59:04 +0200 Subject: [PATCH 28/94] Try to fix CI, no match for QString::arg(QStringRef) --- src/timeline2/TimelineViewManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index b0ed4eab..93a42543 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -82,7 +82,7 @@ TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo & QString body; bool firstLine = true; - for (const auto &line : related.quoted_body.splitRef("\n")) { + for (const auto &line : related.quoted_body.split("\n")) { if (firstLine) { firstLine = false; body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); From d1fffd66170d7548926b205dda7d8de81bef3384 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 18 Sep 2019 20:34:30 +0200 Subject: [PATCH 29/94] Implement simple scroll state handling --- resources/qml/TimelineView.qml | 47 ++++++++++++++++++++++++++- src/timeline2/TimelineModel.cpp | 19 +++++++++++ src/timeline2/TimelineModel.h | 12 +++++++ src/timeline2/TimelineViewManager.cpp | 1 - 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 4f10f352..e1aa2738 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -2,6 +2,7 @@ import QtQuick 2.6 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.5 import QtGraphicalEffects 1.0 +import QtQuick.Window 2.2 import com.github.nheko 1.0 @@ -28,14 +29,51 @@ Rectangle { visible: timelineManager.timeline != null anchors.fill: parent + model: timelineManager.timeline + + onModelChanged: { + if (model) { + currentIndex = model.currentIndex + if (model.currentIndex == count - 1) { + positionViewAtEnd() + } else { + positionViewAtIndex(model.currentIndex, ListView.End) + } + } + } + ScrollBar.vertical: ScrollBar { id: scrollbar anchors.top: parent.top anchors.right: parent.right anchors.bottom: parent.bottom + onPressedChanged: if (!pressed) chat.updatePosition() } - model: timelineManager.timeline + property bool atBottom: false + onCountChanged: { + if (atBottom && Window.active) { + var newIndex = count - 1 // last index + positionViewAtEnd() + currentIndex = newIndex + model.currentIndex = newIndex + } + } + + function updatePosition() { + for (var y = chat.contentY + chat.height; y > chat.height; y -= 5) { + var i = chat.itemAt(100, y); + if (!i) continue; + if (!i.isFullyVisible()) continue; + chat.model.currentIndex = i.getIndex(); + chat.currentIndex = i.getIndex() + atBottom = i.getIndex() == count - 1; + console.log("bottom:" + atBottom) + break; + } + } + onMovementEnded: updatePosition() + spacing: 4 delegate: RowLayout { anchors.leftMargin: 52 @@ -43,6 +81,13 @@ Rectangle { anchors.right: parent.right anchors.rightMargin: scrollbar.width + function isFullyVisible() { + return (y - chat.contentY - 1) + height < chat.height + } + function getIndex() { + return index; + } + Loader { id: loader Layout.fillWidth: true diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 46a33add..7a2edda4 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -625,3 +625,22 @@ TimelineModel::replyAction(QString id) emit ChatPage::instance()->messageReply(related); } + +int +TimelineModel::idToIndex(QString id) const +{ + if (id.isEmpty()) + return -1; + for (int i = 0; i < (int)eventOrder.size(); i++) + if (id == eventOrder[i]) + return i; + return -1; +} + +QString +TimelineModel::indexToId(int index) const +{ + if (index < 0 || index >= (int)eventOrder.size()) + return ""; + return eventOrder[index]; +} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index b2481668..17f83323 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -81,6 +81,8 @@ struct DecryptionResult class TimelineModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY( + int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) public: explicit TimelineModel(QString room_id, QObject *parent = 0); @@ -112,6 +114,8 @@ public: Q_INVOKABLE QString escapeEmoji(QString str) const; Q_INVOKABLE void viewRawMessage(QString id) const; Q_INVOKABLE void replyAction(QString id); + Q_INVOKABLE int idToIndex(QString id) const; + Q_INVOKABLE QString indexToId(int index) const; void addEvents(const mtx::responses::Timeline &events); template @@ -119,6 +123,12 @@ public: public slots: void fetchHistory(); + void setCurrentIndex(int index) + { + currentId = indexToId(index); + emit currentIndexChanged(index); + } + int currentIndex() const { return idToIndex(currentId); } private slots: // Add old events at the top of the timeline. @@ -129,6 +139,7 @@ signals: void oldMessagesRetrieved(const mtx::responses::Messages &res); void messageFailed(const std::string txn_id); void messageSent(const std::string txn_id, std::string event_id); + void currentIndexChanged(int index); private: DecryptionResult decryptEvent( @@ -146,6 +157,7 @@ private: bool paginationInProgress = false; QHash userColors; + QString currentId; }; template diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 93a42543..8233d33e 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -14,7 +14,6 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) 0, "MtxEvent", "Can't instantiate enum!"); - view = new QQuickView(); container = QWidget::createWindowContainer(view, parent); container->setMinimumSize(200, 200); From 5200db17e9d2b816389b28f8587a03c6bf1b4059 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 18 Sep 2019 21:09:46 +0200 Subject: [PATCH 30/94] Implement basic placeholder and disable unimplemented event types --- resources/qml/TimelineView.qml | 24 ++++++++++++------------ resources/qml/delegates/placeholder.qml | 10 ++++++++++ resources/res.qrc | 1 + 3 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 resources/qml/delegates/placeholder.qml diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index e1aa2738..f82cf60a 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -95,21 +95,21 @@ Rectangle { height: item.height source: switch(model.type) { - case MtxEvent.Aliases: return "delegates/Aliases.qml" - case MtxEvent.Avatar: return "delegates/Avatar.qml" - case MtxEvent.CanonicalAlias: return "delegates/CanonicalAlias.qml" - case MtxEvent.Create: return "delegates/Create.qml" - case MtxEvent.GuestAccess: return "delegates/GuestAccess.qml" - case MtxEvent.HistoryVisibility: return "delegates/HistoryVisibility.qml" - case MtxEvent.JoinRules: return "delegates/JoinRules.qml" - case MtxEvent.Member: return "delegates/Member.qml" - case MtxEvent.Name: return "delegates/Name.qml" - case MtxEvent.PowerLevels: return "delegates/PowerLevels.qml" - case MtxEvent.Topic: return "delegates/Topic.qml" + //case MtxEvent.Aliases: return "delegates/Aliases.qml" + //case MtxEvent.Avatar: return "delegates/Avatar.qml" + //case MtxEvent.CanonicalAlias: return "delegates/CanonicalAlias.qml" + //case MtxEvent.Create: return "delegates/Create.qml" + //case MtxEvent.GuestAccess: return "delegates/GuestAccess.qml" + //case MtxEvent.HistoryVisibility: return "delegates/HistoryVisibility.qml" + //case MtxEvent.JoinRules: return "delegates/JoinRules.qml" + //case MtxEvent.Member: return "delegates/Member.qml" + //case MtxEvent.Name: return "delegates/Name.qml" + //case MtxEvent.PowerLevels: return "delegates/PowerLevels.qml" + //case MtxEvent.Topic: return "delegates/Topic.qml" case MtxEvent.NoticeMessage: return "delegates/NoticeMessage.qml" case MtxEvent.TextMessage: return "delegates/TextMessage.qml" case MtxEvent.ImageMessage: return "delegates/ImageMessage.qml" - case MtxEvent.VideoMessage: return "delegates/VideoMessage.qml" + //case MtxEvent.VideoMessage: return "delegates/VideoMessage.qml" case MtxEvent.Redacted: return "delegates/Redacted.qml" default: return "delegates/placeholder.qml" } diff --git a/resources/qml/delegates/placeholder.qml b/resources/qml/delegates/placeholder.qml new file mode 100644 index 00000000..d17184f3 --- /dev/null +++ b/resources/qml/delegates/placeholder.qml @@ -0,0 +1,10 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.5 + +Label { + text: qsTr("unimplemented event: ") + eventData.type + textFormat: Text.PlainText + wrapMode: Text.Wrap + width: parent.width + color: inactiveColors.text +} diff --git a/resources/res.qrc b/resources/res.qrc index 0d55e70d..6eb61e3d 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -121,5 +121,6 @@ qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml qml/delegates/Redacted.qml + qml/delegates/placeholder.qml From 240b3a566b8f73261bd6c48ae7480800136e3ec2 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 18 Sep 2019 22:58:25 +0200 Subject: [PATCH 31/94] Add send/received indicator --- resources/qml/StatusIndicator.qml | 44 +++++++++++++++++++++++++++++ resources/qml/TimelineView.qml | 5 ++++ resources/res.qrc | 1 + src/timeline2/TimelineModel.cpp | 43 +++++++++++++++++++++++++++- src/timeline2/TimelineModel.h | 47 +++++++++++++++++++++++++++---- 5 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 resources/qml/StatusIndicator.qml diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml new file mode 100644 index 00000000..0b14e246 --- /dev/null +++ b/resources/qml/StatusIndicator.qml @@ -0,0 +1,44 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.5 +import QtGraphicalEffects 1.0 +import com.github.nheko 1.0 + +Rectangle { + id: indicator + property int state: 0 + color: "transparent" + width: 16 + height: 16 + ToolTip.visible: ma.containsMouse + ToolTip.text: switch (state) { + case MtxEvent.Failed: return qsTr("Failed") + case MtxEvent.Sent: return qsTr("Sent") + case MtxEvent.Received: return qsTr("Received") + case MtxEvent.Read: return qsTr("Read") + default: return qsTr("Empty") + } + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: switch (indicator.state) { + case MtxEvent.Failed: return "qrc:/icons/icons/ui/remove-symbol.png" + case MtxEvent.Sent: return "qrc:/icons/icons/ui/clock.png" + case MtxEvent.Received: return "qrc:/icons/icons/ui/checkmark.png" + case MtxEvent.Read: return "qrc:/icons/icons/ui/double-tick-indicator.png" + default: return "" + } + } + ColorOverlay { + anchors.fill: stateImg + source: stateImg + color: colors.buttonText + } +} + diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index f82cf60a..5eb00b06 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -116,6 +116,11 @@ Rectangle { property variant eventData: model } + StatusIndicator { + state: model.state + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } Button { Layout.alignment: Qt.AlignRight | Qt.AlignTop diff --git a/resources/res.qrc b/resources/res.qrc index 6eb61e3d..a9cf885b 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -117,6 +117,7 @@ qml/TimelineView.qml qml/Avatar.qml + qml/StatusIndicator.qml qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 7a2edda4..13429c3e 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -240,6 +240,35 @@ TimelineModel::TimelineModel(QString room_id, QObject *parent) { connect( this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); + connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { + pending.remove(txn_id); + failed.insert(txn_id); + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Failed index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Sent index out of range"); + return; + } + eventOrder[idx] = event_id; + auto ev = events.value(txn_id); + ev = boost::apply_visitor( + [event_id](const auto &e) -> mtx::events::collections::TimelineEvents { + auto eventCopy = e; + eventCopy.event_id = event_id.toStdString(); + return eventCopy; + }, + ev); + events.remove(txn_id); + events.insert(event_id, ev); + emit dataChanged(index(idx, 0), index(idx, 0)); + }); } QHash @@ -258,6 +287,7 @@ TimelineModel::roleNames() const {Width, "width"}, {ProportionalHeight, "proportionalHeight"}, {Id, "id"}, + {State, "state"}, }; } int @@ -341,6 +371,13 @@ TimelineModel::data(const QModelIndex &index, int role) const [](const auto &e) -> double { return eventPropHeight(e); }, event)); case Id: return id; + case State: + if (failed.contains(id)) + return qml_mtx_events::Failed; + else if (pending.contains(id)) + return qml_mtx_events::Sent; + else + return qml_mtx_events::Received; default: return QVariant(); } @@ -378,8 +415,12 @@ TimelineModel::internalAddEvents( QString id = boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); - if (this->events.contains(id)) + if (this->events.contains(id)) { + this->events.insert(id, e); + int idx = idToIndex(id); + emit dataChanged(index(idx, 0), index(idx, 0)); continue; + } if (auto redaction = boost::get>(&e)) { diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 17f83323..b651708d 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "Logging.h" #include "MatrixClient.h" @@ -68,6 +69,21 @@ enum EventType UnknownMessage, }; Q_ENUM_NS(EventType) + +enum EventState +{ + //! The plaintext message was received by the server. + Received, + //! At least one of the participants has read the message. + Read, + //! The client sent the message. Not yet received. + Sent, + //! When the message is loaded from cache or backfill. + Empty, + //! When the message failed to send + Failed, +}; +Q_ENUM_NS(EventState) } struct DecryptionResult @@ -101,6 +117,7 @@ public: Width, ProportionalHeight, Id, + State, }; QHash roleNames() const override; @@ -137,8 +154,8 @@ private slots: signals: void oldMessagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(const std::string txn_id); - void messageSent(const std::string txn_id, std::string event_id); + void messageFailed(QString txn_id); + void messageSent(QString txn_id, QString event_id); void currentIndexChanged(int index); private: @@ -148,6 +165,7 @@ private: const std::vector &timeline); QHash events; + QSet pending, failed; std::vector eventOrder; QString room_id_; @@ -164,20 +182,37 @@ template void TimelineModel::sendMessage(const T &msg) { - auto txn_id = http::client()->generate_txn_id(); + auto txn_id = http::client()->generate_txn_id(); + mtx::events::RoomEvent msgCopy = {}; + msgCopy.content = msg; + msgCopy.type = mtx::events::EventType::RoomMessage; + msgCopy.event_id = txn_id; + msgCopy.sender = http::client()->user_id().to_string(); + msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + internalAddEvents({msgCopy}); + + QString txn_id_qstr = QString::fromStdString(txn_id); + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size())); + pending.insert(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); + endInsertRows(); + http::client()->send_room_message( room_id_.toStdString(), txn_id, msg, - [this, txn_id](const mtx::responses::EventId &res, mtx::http::RequestErr err) { + [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { if (err) { const int status_code = static_cast(err->status_code); nhlog::net()->warn("[{}] failed to send message: {} {}", txn_id, err->matrix_error.error, status_code); - emit messageFailed(txn_id); + emit messageFailed(txn_id_qstr); } - emit messageSent(txn_id, res.event_id.to_string()); + emit messageSent(txn_id_qstr, QString::fromStdString(res.event_id.to_string())); }); } From d34067a25792b5d69b2ce3192486189f0db12abb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 18 Sep 2019 23:37:30 +0200 Subject: [PATCH 32/94] Enable read receipts action and sync read receipts from cache --- resources/qml/TimelineView.qml | 1 + src/timeline2/TimelineModel.cpp | 25 ++++++++++++++++++++++++- src/timeline2/TimelineModel.h | 5 +++-- src/timeline2/TimelineViewManager.cpp | 10 ++++++++++ src/timeline2/TimelineViewManager.h | 2 +- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 5eb00b06..91b3f173 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -180,6 +180,7 @@ Rectangle { MenuItem { text: "Read receipts" + onTriggered: chat.model.readReceiptsAction(model.id) } MenuItem { text: "Mark as read" diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 13429c3e..d0daae25 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -7,6 +7,7 @@ #include "ChatPage.h" #include "Logging.h" +#include "MainWindow.h" #include "Olm.h" #include "Utils.h" #include "dialogs/RawMessage.h" @@ -376,6 +377,8 @@ TimelineModel::data(const QModelIndex &index, int role) const return qml_mtx_events::Failed; else if (pending.contains(id)) return qml_mtx_events::Sent; + else if (read.contains(id)) + return qml_mtx_events::Read; else return qml_mtx_events::Received; default: @@ -664,7 +667,13 @@ TimelineModel::replyAction(QString id) if (related.quoted_body.isEmpty()) return; - emit ChatPage::instance()->messageReply(related); + ChatPage::instance()->messageReply(related); +} + +void +TimelineModel::readReceiptsAction(QString id) const +{ + MainWindow::instance()->openReadReceiptsDialog(id); } int @@ -685,3 +694,17 @@ TimelineModel::indexToId(int index) const return ""; return eventOrder[index]; } + +void +TimelineModel::markEventsAsRead(const std::vector &event_ids) +{ + for (const auto &id : event_ids) { + read.insert(id); + int idx = idToIndex(id); + if (idx < 0) { + nhlog::ui()->warn("Read index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + } +} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index b651708d..2cd22661 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -131,6 +131,7 @@ public: Q_INVOKABLE QString escapeEmoji(QString str) const; Q_INVOKABLE void viewRawMessage(QString id) const; Q_INVOKABLE void replyAction(QString id); + Q_INVOKABLE void readReceiptsAction(QString id) const; Q_INVOKABLE int idToIndex(QString id) const; Q_INVOKABLE QString indexToId(int index) const; @@ -146,10 +147,10 @@ public slots: emit currentIndexChanged(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: @@ -165,7 +166,7 @@ private: const std::vector &timeline); QHash events; - QSet pending, failed; + QSet pending, failed, read; std::vector eventOrder; QString room_id_; diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 8233d33e..18297370 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -52,6 +52,16 @@ TimelineViewManager::setHistoryView(const QString &room_id) } } +void +TimelineViewManager::updateReadReceipts(const QString &room_id, + const std::vector &event_ids) +{ + auto room = models.find(room_id); + if (room != models.end()) { + room.value()->markEventsAsRead(event_ids); + } +} + void TimelineViewManager::initWithMessages(const std::map &msgs) { diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index c2d1a8c0..52070b97 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -40,7 +40,7 @@ signals: void activeTimelineChanged(TimelineModel *timeline); public slots: - void updateReadReceipts(const QString &room_id, const std::vector &event_ids) {} + void updateReadReceipts(const QString &room_id, const std::vector &event_ids); void removeTimelineEvent(const QString &room_id, const QString &event_id) {} void initWithMessages(const std::map &msgs); From 6c7e6b0e86ae78479b5f87b8440a8d10f99f14e0 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 19 Sep 2019 21:47:16 +0200 Subject: [PATCH 33/94] Fix read indicator --- resources/qml/StatusIndicator.qml | 4 ++-- src/timeline2/TimelineModel.cpp | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 0b14e246..440a7e47 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -9,13 +9,13 @@ Rectangle { color: "transparent" width: 16 height: 16 - ToolTip.visible: ma.containsMouse + ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty ToolTip.text: switch (state) { case MtxEvent.Failed: return qsTr("Failed") case MtxEvent.Sent: return qsTr("Sent") case MtxEvent.Received: return qsTr("Received") case MtxEvent.Read: return qsTr("Read") - default: return qsTr("Empty") + default: return "" } MouseArea{ id: ma diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index d0daae25..1c9070b1 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -268,6 +268,10 @@ TimelineModel::TimelineModel(QString room_id, QObject *parent) ev); events.remove(txn_id); events.insert(event_id, ev); + + // ask to be notified for read receipts + cache::client()->addPendingReceipt(room_id_, event_id); + emit dataChanged(index(idx, 0), index(idx, 0)); }); } @@ -373,11 +377,17 @@ TimelineModel::data(const QModelIndex &index, int role) const case Id: return id; case State: - if (failed.contains(id)) + // only show read receipts for messages not from us + if (boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, + event) + .toStdString() != http::client()->user_id().to_string()) + return qml_mtx_events::Empty; + else if (failed.contains(id)) return qml_mtx_events::Failed; else if (pending.contains(id)) return qml_mtx_events::Sent; - else if (read.contains(id)) + else if (read.contains(id) || + cache::client()->readReceipts(id, room_id_).size() > 1) return qml_mtx_events::Read; else return qml_mtx_events::Received; @@ -695,6 +705,7 @@ TimelineModel::indexToId(int index) const return eventOrder[index]; } +// Note: this will only be called for our messages void TimelineModel::markEventsAsRead(const std::vector &event_ids) { From c8315d792bf321ed4189fc431ecd640513a808f3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 19 Sep 2019 21:53:24 +0200 Subject: [PATCH 34/94] Make avatar in timeline smaller --- resources/qml/TimelineView.qml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 91b3f173..a04c0c7f 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -11,6 +11,8 @@ Rectangle { SystemPalette { id: colors; colorGroup: SystemPalette.Active } SystemPalette { id: inactiveColors; colorGroup: SystemPalette.Disabled } + property int avatarSize: 32 + color: colors.window Text { @@ -76,7 +78,7 @@ Rectangle { spacing: 4 delegate: RowLayout { - anchors.leftMargin: 52 + anchors.leftMargin: avatarSize + 4 anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: scrollbar.width @@ -239,8 +241,8 @@ Rectangle { height: userName.height spacing: 4 Avatar { - width: 48 - height: 48 + width: avatarSize + height: avatarSize url: chat.model.avatarUrl(section.split(" ")[0]).replace("mxc://", "image://MxcImage/") displayName: chat.model.displayName(section.split(" ")[0]) } From bb60976e7e9ca2a3f9ad54a571564de2b42c5d5c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 19 Sep 2019 22:44:25 +0200 Subject: [PATCH 35/94] Reenable encrypted messages --- src/timeline2/TimelineModel.cpp | 301 ++++++++++++++++++++++++++++++++ src/timeline2/TimelineModel.h | 57 ++++-- 2 files changed, 342 insertions(+), 16 deletions(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 1c9070b1..be82cf7e 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -719,3 +719,304 @@ TimelineModel::markEventsAsRead(const std::vector &event_ids) emit dataChanged(index(idx, 0), index(idx, 0)); } } + +void +TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content) +{ + const auto room_id = room_id_.toStdString(); + + using namespace mtx::events; + using namespace mtx::identifiers; + + json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; + + try { + // Check if we have already an outbound megolm session then we can use. + if (cache::client()->outboundMegolmSessionExists(room_id)) { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc.dump()); + + http::client()->send_room_message( + room_id, + txn_id, + data, + [this, txn_id](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(QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + return; + } + + nhlog::ui()->debug("creating new outbound megolm session"); + + // Create a new outbound megolm session. + auto outbound_session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(outbound_session.get()); + const auto session_key = mtx::crypto::session_key(outbound_session.get()); + + // TODO: needs to be moved in the lib. + auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", room_id}, + {"session_id", session_id}, + {"session_key", session_key}}; + + // Saving the new megolm session. + // TODO: Maybe it's too early to save. + OutboundGroupSessionData session_data; + session_data.session_id = session_id; + session_data.session_key = session_key; + session_data.message_index = 0; // TODO Update me + cache::client()->saveOutboundMegolmSession( + room_id, session_data, std::move(outbound_session)); + + const auto members = cache::client()->roomMembers(room_id); + nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); + + auto keeper = + std::make_shared([megolm_payload, room_id, doc, txn_id, this]() { + try { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc.dump()); + + http::client() + ->send_room_message( + room_id, + txn_id, + data, + [this, txn_id](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( + QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to save megolm outbound session: {}", e.what()); + } + }); + + mtx::requests::QueryKeys req; + for (const auto &member : members) + req.device_keys[member] = {}; + + http::client()->query_keys( + req, + [keeper = std::move(keeper), megolm_payload, 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. + return; + } + + for (const auto &user : res.device_keys) { + // Mapping from a device_id with valid identity keys to the + // generated room_key event used for sharing the megolm session. + std::map room_key_msgs; + std::map deviceKeys; + + room_key_msgs.clear(); + deviceKeys.clear(); + + for (const auto &dev : user.second) { + const auto user_id = ::UserId(dev.second.user_id); + const auto device_id = DeviceId(dev.second.device_id); + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + device_id.get(); + const auto edKey = "ed25519:" + device_id.get(); + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + nhlog::net()->debug( + "ignoring malformed keys for device {}", + device_id.get()); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + try { + if (!mtx::crypto::verify_identity_signature( + json(dev.second), device_id, user_id)) { + nhlog::crypto()->warn( + "failed to verify identity keys: {}", + json(dev.second).dump(2)); + continue; + } + } catch (const json::exception &e) { + nhlog::crypto()->warn( + "failed to parse device key json: {}", + e.what()); + continue; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->warn( + "failed to verify device key json: {}", + e.what()); + continue; + } + + auto room_key = olm::client() + ->create_room_key_event( + user_id, pks.ed25519, megolm_payload) + .dump(); + + room_key_msgs.emplace(device_id, room_key); + deviceKeys.emplace(device_id, pks); + } + + std::vector valid_devices; + valid_devices.reserve(room_key_msgs.size()); + for (auto const &d : room_key_msgs) { + valid_devices.push_back(d.first); + + nhlog::net()->info("{}", d.first); + nhlog::net()->info(" curve25519 {}", + deviceKeys.at(d.first).curve25519); + nhlog::net()->info(" ed25519 {}", + deviceKeys.at(d.first).ed25519); + } + + nhlog::net()->info( + "sending claim request for user {} with {} devices", + user.first, + valid_devices.size()); + + http::client()->claim_keys( + user.first, + valid_devices, + std::bind(&TimelineModel::handleClaimedKeys, + this, + keeper, + room_key_msgs, + deviceKeys, + user.first, + std::placeholders::_1, + std::placeholders::_2)); + + // TODO: Wait before sending the next batch of requests. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + }); + + // TODO: Let the user know about the errors. + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + } +} + +void +TimelineModel::handleClaimedKeys(std::shared_ptr keeper, + const std::map &room_keys, + const std::map &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) +{ + if (err) { + nhlog::net()->warn("claim keys error: {} {} {}", + err->matrix_error.error, + err->parse_error, + static_cast(err->status_code)); + return; + } + + nhlog::net()->debug("claimed keys for {}", user_id); + + if (res.one_time_keys.size() == 0) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + auto retrieved_devices = res.one_time_keys.at(user_id); + + // Payload with all the to_device message to be sent. + json body; + body["messages"][user_id] = json::object(); + + for (const auto &rd : retrieved_devices) { + const auto device_id = rd.first; + nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + + if (pks.find(device_id) == pks.end()) { + nhlog::net()->critical("couldn't find public key for device: {}", + device_id); + continue; + } + + auto id_key = pks.at(device_id).curve25519; + auto s = olm::client()->create_outbound_session(id_key, otk); + + if (room_keys.find(device_id) == room_keys.end()) { + nhlog::net()->critical("couldn't find m.room_key for device: {}", + device_id); + continue; + } + + auto device_msg = olm::client()->create_olm_encrypted_content( + s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); + + try { + cache::client()->saveOlmSession(id_key, std::move(s)); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", + e.what()); + } + + body["messages"][user_id][device_id] = device_msg; + } + + nhlog::net()->info("send_to_device: {}", user_id); + + http::client()->send_to_device( + "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + + (void)keeper; + }); +} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 2cd22661..7723ef66 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -8,6 +8,7 @@ #include #include +#include "Cache.h" #include "Logging.h" #include "MatrixClient.h" @@ -86,6 +87,19 @@ enum EventState 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. @@ -164,6 +178,13 @@ private: 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); QHash events; QSet pending, failed, read; @@ -200,20 +221,24 @@ TimelineModel::sendMessage(const T &msg) this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); endInsertRows(); - 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())); - }); + 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())); + }); } From a5ccd00be0d58970a37944a68a91b7902cbf450f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 19 Sep 2019 22:45:51 +0200 Subject: [PATCH 36/94] Remove noisy decrypted message --- src/timeline2/TimelineModel.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index be82cf7e..9537649b 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -638,8 +638,6 @@ TimelineModel::decryptEvent(const mtx::events::EncryptedEventdebug("decrypted event: {}", e.event_id); - json event_array = json::array(); event_array.push_back(body); From 82091999c4fc81412c726d28a339b305709bacd0 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 19 Sep 2019 23:02:56 +0200 Subject: [PATCH 37/94] Add lock to encrypted messages --- resources/qml/EncryptionIndicator.qml | 30 +++++++++++++++++++++++++++ resources/qml/TimelineView.qml | 6 ++++++ resources/res.qrc | 1 + src/timeline2/TimelineModel.cpp | 6 ++++++ src/timeline2/TimelineModel.h | 1 + 5 files changed, 44 insertions(+) create mode 100644 resources/qml/EncryptionIndicator.qml diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml new file mode 100644 index 00000000..6cb5138a --- /dev/null +++ b/resources/qml/EncryptionIndicator.qml @@ -0,0 +1,30 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.5 +import QtGraphicalEffects 1.0 +import com.github.nheko 1.0 + +Rectangle { + id: indicator + color: "transparent" + width: 16 + height: 16 + ToolTip.visible: ma.containsMouse && indicator.visible + ToolTip.text: qsTr("Encrypted") + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + anchors.fill: parent + source: "qrc:/icons/icons/ui/lock.png" + } + ColorOverlay { + anchors.fill: stateImg + source: stateImg + color: colors.buttonText + } +} + diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index a04c0c7f..ee4b53b9 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -124,6 +124,12 @@ Rectangle { Layout.preferredHeight: 16 } + EncryptionIndicator { + visible: model.isEncrypted + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } + Button { Layout.alignment: Qt.AlignRight | Qt.AlignTop id: replyButton diff --git a/resources/res.qrc b/resources/res.qrc index a9cf885b..02b4c0c0 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -118,6 +118,7 @@ qml/TimelineView.qml qml/Avatar.qml qml/StatusIndicator.qml + qml/EncryptionIndicator.qml qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 9537649b..36b768ba 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -293,6 +293,7 @@ TimelineModel::roleNames() const {ProportionalHeight, "proportionalHeight"}, {Id, "id"}, {State, "state"}, + {IsEncrypted, "isEncrypted"}, }; } int @@ -391,6 +392,11 @@ TimelineModel::data(const QModelIndex &index, int role) const return qml_mtx_events::Read; else return qml_mtx_events::Received; + case IsEncrypted: { + auto tempEvent = events[id]; + return boost::get>( + &tempEvent) != nullptr; + } default: return QVariant(); } diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 7723ef66..3d55f206 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -132,6 +132,7 @@ public: ProportionalHeight, Id, State, + IsEncrypted, }; QHash roleNames() const override; From 9b18440b4f989da4fcd764f33291d17dbbcb82e3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 28 Sep 2019 11:07:58 +0200 Subject: [PATCH 38/94] Reenable ImageOverlay --- resources/qml/delegates/ImageMessage.qml | 6 ++++++ src/timeline2/TimelineViewManager.cpp | 25 +++++++++++++++++++++++- src/timeline2/TimelineViewManager.h | 4 ++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index a3bc78e5..f4f5e369 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -5,10 +5,16 @@ Item { height: 300 * eventData.proportionalHeight Image { + id: img anchors.fill: parent source: eventData.url.replace("mxc://", "image://MxcImage/") asynchronous: true fillMode: Image.PreserveAspectFit + + MouseArea { + anchors.fill: parent + onClicked: timelineManager.openImageOverlay(img.source) + } } } diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 18297370..bf09ef5a 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -5,8 +5,10 @@ #include "Logging.h" #include "MxcImageProvider.h" +#include "dialogs/ImageOverlay.h" TimelineViewManager::TimelineViewManager(QWidget *parent) + : imgProvider(new MxcImageProvider()) { qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, "com.github.nheko", @@ -18,7 +20,7 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) container = QWidget::createWindowContainer(view, parent); container->setMinimumSize(200, 200); view->rootContext()->setContextProperty("timelineManager", this); - view->engine()->addImageProvider("MxcImage", new MxcImageProvider()); + view->engine()->addImageProvider("MxcImage", imgProvider); view->setSource(QUrl("qrc:///qml/TimelineView.qml")); } @@ -52,6 +54,27 @@ TimelineViewManager::setHistoryView(const QString &room_id) } } +void +TimelineViewManager::openImageOverlay(QString url) const +{ + QQuickImageResponse *imgResponse = + imgProvider->requestImageResponse(url.remove("image://mxcimage/"), QSize()); + connect(imgResponse, &QQuickImageResponse::finished, this, [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, + // &ImageItem::saveAs); + Q_UNUSED(imgDialog); + }); +} + void TimelineViewManager::updateReadReceipts(const QString &room_id, const std::vector &event_ids) diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 52070b97..68f6ddb0 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -15,6 +15,8 @@ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" +class MxcImageProvider; + class TimelineViewManager : public QObject { Q_OBJECT @@ -33,6 +35,7 @@ public: void clearAll() { models.clear(); } Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } + Q_INVOKABLE void openImageOverlay(QString url) const; signals: void clearRoomMessageCount(QString roomid); @@ -75,6 +78,7 @@ private: QQuickView *view; QWidget *container; TimelineModel *timeline_ = nullptr; + MxcImageProvider *imgProvider; QHash> models; }; From e2d733a01a1c936d22ec6c67b2f3b57ac0afdabb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 29 Sep 2019 10:45:35 +0200 Subject: [PATCH 39/94] Restore saving of media --- .gitignore | 1 + CMakeLists.txt | 1 - resources/qml/TimelineView.qml | 13 +++- resources/qml/delegates/ImageMessage.qml | 2 +- src/MatrixClient.h | 10 --- src/timeline2/TimelineModel.cpp | 55 ++++++++++++++ src/timeline2/TimelineModel.h | 2 + src/timeline2/TimelineViewManager.cpp | 96 ++++++++++++++++++++---- src/timeline2/TimelineViewManager.h | 25 +++++- 9 files changed, 173 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 0f61a911..2d772e58 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ ui_*.h # Vim *.swp +*.swo #####=== CMake ===##### diff --git a/CMakeLists.txt b/CMakeLists.txt index d386efbf..1cf34c32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -375,7 +375,6 @@ qt5_wrap_cpp(MOC_HEADERS src/CommunitiesList.h src/LoginPage.h src/MainWindow.h - src/MatrixClient.h src/InviteeItem.h src/QuickSwitcher.h src/RegisterPage.h diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index ee4b53b9..051ea915 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -187,18 +187,23 @@ Rectangle { id: contextMenu MenuItem { - text: "Read receipts" + text: qsTr("Read receipts") onTriggered: chat.model.readReceiptsAction(model.id) } MenuItem { - text: "Mark as read" + text: qsTr("Mark as read") } MenuItem { - text: "View raw message" + text: qsTr("View raw message") onTriggered: chat.model.viewRawMessage(model.id) } MenuItem { - text: "Redact message" + text: qsTr("Redact message") + } + MenuItem { + visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage + text: qsTr("Save as") + onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) } } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index f4f5e369..3f5c00bf 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -14,7 +14,7 @@ Item { MouseArea { anchors.fill: parent - onClicked: timelineManager.openImageOverlay(img.source) + onClicked: timelineManager.openImageOverlay(eventData.url, eventData.filename, eventData.mimetype, eventData.type) } } } diff --git a/src/MatrixClient.h b/src/MatrixClient.h index 2af57267..c77b1183 100644 --- a/src/MatrixClient.h +++ b/src/MatrixClient.h @@ -20,16 +20,6 @@ Q_DECLARE_METATYPE(nlohmann::json) Q_DECLARE_METATYPE(std::vector) Q_DECLARE_METATYPE(std::vector) -class MediaProxy : public QObject -{ - Q_OBJECT - -signals: - void imageDownloaded(const QPixmap &); - void imageSaved(const QString &, const QByteArray &); - void fileDownloaded(const QByteArray &); -}; - namespace http { mtx::http::Client * client(); diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 36b768ba..b702686e 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -104,6 +104,53 @@ eventUrl(const mtx::events::RoomEvent &e) return QString::fromStdString(e.content.url); } +template +QString +eventFilename(const T &) +{ + return ""; +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + if (!e.content.filename.empty()) + return QString::fromStdString(e.content.filename); + return QString::fromStdString(e.content.body); +} + +template +QString +eventMimeType(const T &) +{ + return QString(); +} +template +auto +eventMimeType(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.info.mimetype); +} + template qml_mtx_events::EventType toRoomEventType(const mtx::events::Event &e) @@ -288,6 +335,8 @@ TimelineModel::roleNames() const {UserName, "userName"}, {Timestamp, "timestamp"}, {Url, "url"}, + {Filename, "filename"}, + {MimeType, "mimetype"}, {Height, "height"}, {Width, "width"}, {ProportionalHeight, "proportionalHeight"}, @@ -366,6 +415,12 @@ TimelineModel::data(const QModelIndex &index, int role) const case Url: return QVariant(boost::apply_visitor( [](const auto &e) -> QString { return eventUrl(e); }, event)); + case Filename: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventFilename(e); }, event)); + case MimeType: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventMimeType(e); }, event)); case Height: return QVariant(boost::apply_visitor( [](const auto &e) -> qulonglong { return eventHeight(e); }, event)); diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 3d55f206..e10a0b6e 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -127,6 +127,8 @@ public: UserName, Timestamp, Url, + Filename, + MimeType, Height, Width, ProportionalHeight, diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index bf09ef5a..eed0682d 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -1,6 +1,8 @@ #include "TimelineViewManager.h" +#include #include +#include #include #include "Logging.h" @@ -55,24 +57,88 @@ TimelineViewManager::setHistoryView(const QString &room_id) } void -TimelineViewManager::openImageOverlay(QString url) const +TimelineViewManager::openImageOverlay(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const { QQuickImageResponse *imgResponse = - imgProvider->requestImageResponse(url.remove("image://mxcimage/"), QSize()); - connect(imgResponse, &QQuickImageResponse::finished, this, [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()); + 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, - // &ImageItem::saveAs); - Q_UNUSED(imgDialog); - }); + 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(), data.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); } void diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 68f6ddb0..687ae24e 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -35,7 +35,30 @@ public: void clearAll() { models.clear(); } Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } - Q_INVOKABLE void openImageOverlay(QString url) const; + 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; + // 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); + } signals: void clearRoomMessageCount(QString roomid); From 0d3c9390c67d4f0fefdfa192009291ff024ecce8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 29 Sep 2019 11:28:55 +0200 Subject: [PATCH 40/94] Rename initialize to sync, since it does the same thing --- src/ChatPage.cpp | 4 ++-- src/timeline2/TimelineViewManager.cpp | 3 ++- src/timeline2/TimelineViewManager.h | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 594a41c2..e5d4c9be 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -566,7 +566,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::initializeViews, view_manager_, - [this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); }); + [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); }); connect(this, &ChatPage::initializeEmptyViews, view_manager_, @@ -582,7 +582,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) nhlog::db()->error("failed to retrieve invites: {}", e.what()); } - view_manager_->initialize(rooms); + view_manager_->sync(rooms); removeLeftRooms(rooms.leave); bool hasNotifications = false; diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index eed0682d..4ec089fa 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -27,9 +27,10 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) } void -TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) +TimelineViewManager::sync(const mtx::responses::Rooms &rooms) { for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { + // 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); } diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 687ae24e..9fcbc2f8 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -28,10 +28,9 @@ public: TimelineViewManager(QWidget *parent = 0); QWidget *getWidget() const { return container; } - void initialize(const mtx::responses::Rooms &rooms); + void sync(const mtx::responses::Rooms &rooms); void addRoom(const QString &room_id); - void sync(const mtx::responses::Rooms &rooms) {} void clearAll() { models.clear(); } Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } From aee29c6ed5b9cc9b60714cfb2f744109651ac035 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 29 Sep 2019 12:29:17 +0200 Subject: [PATCH 41/94] Reenable redactions --- resources/qml/TimelineView.qml | 1 + src/ChatPage.cpp | 5 ----- src/ChatPage.h | 2 -- src/timeline2/TimelineModel.cpp | 22 ++++++++++++++++++++++ src/timeline2/TimelineModel.h | 3 +++ src/timeline2/TimelineViewManager.h | 1 - 6 files changed, 26 insertions(+), 8 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 051ea915..35dcae03 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -199,6 +199,7 @@ Rectangle { } MenuItem { text: qsTr("Redact message") + onTriggered: chat.model.redactEvent(model.id) } MenuItem { visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index e5d4c9be..b8f312ac 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -115,11 +115,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) contentLayout_->addWidget(top_bar_); contentLayout_->addWidget(view_manager_->getWidget()); - connect(this, - &ChatPage::removeTimelineEvent, - view_manager_, - &TimelineViewManager::removeTimelineEvent); - // Splitter splitter->addWidget(sideBar_); splitter->addWidget(content_); diff --git a/src/ChatPage.h b/src/ChatPage.h index e41ae1ae..1898f1a7 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -125,8 +125,6 @@ signals: void showUserSettingsPage(); void showOverlayProgressBar(); - void removeTimelineEvent(const QString &room_id, const QString &event_id); - void ownProfileOk(); void setUserDisplayName(const QString &name); void setUserAvatar(const QString &avatar); diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index b702686e..f9a8358f 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -321,6 +321,9 @@ TimelineModel::TimelineModel(QString room_id, QObject *parent) emit dataChanged(index(idx, 0), index(idx, 0)); }); + connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { + emit ChatPage::instance()->showNotification(msg); + }); } QHash @@ -745,6 +748,25 @@ TimelineModel::readReceiptsAction(QString id) const MainWindow::instance()->openReadReceiptsDialog(id); } +void +TimelineModel::redactEvent(QString id) +{ + if (!id.isEmpty()) + http::client()->redact_event( + room_id_.toStdString(), + id.toStdString(), + [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit redactionFailed( + tr("Message redaction failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit eventRedacted(id); + }); +} + int TimelineModel::idToIndex(QString id) const { diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index e10a0b6e..ffe5aecb 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -149,6 +149,7 @@ public: Q_INVOKABLE void viewRawMessage(QString id) 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; @@ -175,6 +176,8 @@ signals: 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( diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 9fcbc2f8..38d68f16 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -66,7 +66,6 @@ signals: public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); - void removeTimelineEvent(const QString &room_id, const QString &event_id) {} void initWithMessages(const std::map &msgs); void setHistoryView(const QString &room_id); From 1dd1a19b06861e2ab0fc282af143e792080bbbdb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 3 Oct 2019 18:07:01 +0200 Subject: [PATCH 42/94] Update roomlist on new messages --- src/timeline2/TimelineModel.cpp | 24 +++++++++++++++++++++++- src/timeline2/TimelineModel.h | 7 ++++++- src/timeline2/TimelineViewManager.cpp | 3 ++- src/timeline2/TimelineViewManager.h | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index f9a8358f..db9ce555 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -9,6 +9,7 @@ #include "Logging.h" #include "MainWindow.h" #include "Olm.h" +#include "TimelineViewManager.h" #include "Utils.h" #include "dialogs/RawMessage.h" @@ -282,9 +283,10 @@ eventPropHeight(const mtx::events::RoomEvent &e) } } -TimelineModel::TimelineModel(QString room_id, QObject *parent) +TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) : QAbstractListModel(parent) , room_id_(room_id) + , manager_(manager) { connect( this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); @@ -481,6 +483,26 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) static_cast(this->eventOrder.size() + ids.size() - 1)); this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); endInsertRows(); + + for (auto id = ids.rbegin(); id != ids.rend(); id++) { + auto event = events.value(*id); + if (auto e = boost::get>( + &event)) { + event = decryptEvent(*e).event; + } + + auto type = boost::apply_visitor( + [](const auto &e) -> mtx::events::EventType { return e.type; }, event); + if (type == mtx::events::EventType::RoomMessage || + type == mtx::events::EventType::Sticker) { + auto description = utils::getMessageDescription( + event, + QString::fromStdString(http::client()->user_id().to_string()), + room_id_); + emit manager_->updateRoomsLastMessage(room_id_, description); + break; + } + } } std::vector diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index ffe5aecb..9b861010 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -108,6 +108,8 @@ struct DecryptionResult bool isDecrypted = false; }; +class TimelineViewManager; + class TimelineModel : public QAbstractListModel { Q_OBJECT @@ -115,7 +117,7 @@ class TimelineModel : public QAbstractListModel int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) public: - explicit TimelineModel(QString room_id, QObject *parent = 0); + explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0); enum Roles { @@ -145,6 +147,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 escapeEmoji(QString str) const; Q_INVOKABLE void viewRawMessage(QString id) const; Q_INVOKABLE void replyAction(QString id); @@ -204,6 +207,8 @@ private: QHash userColors; QString currentId; + + TimelineViewManager *manager_; }; template diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 4ec089fa..74e851c4 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -40,7 +40,8 @@ void TimelineViewManager::addRoom(const QString &room_id) { if (!models.contains(room_id)) - models.insert(room_id, QSharedPointer(new TimelineModel(room_id))); + models.insert(room_id, + QSharedPointer(new TimelineModel(this, room_id))); } void diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 38d68f16..a8fcf7ce 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -61,7 +61,7 @@ public: signals: void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(const QString &user, const DescInfo &info); + void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); public slots: From ea12c9f9bc9918884da8f7a930484412b93f9426 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 3 Oct 2019 22:39:56 +0200 Subject: [PATCH 43/94] Add basic read_event support (qml) --- src/timeline2/TimelineModel.cpp | 21 +++++++++++++++++++++ src/timeline2/TimelineModel.h | 6 +----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index db9ce555..83d1e417 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -584,6 +584,27 @@ TimelineModel::fetchHistory() }); } +void +TimelineModel::setCurrentIndex(int index) +{ + auto oldIndex = idToIndex(currentId); + currentId = indexToId(index); + emit currentIndexChanged(index); + + if (oldIndex < index) { + http::client()->read_event(room_id_.toStdString(), + currentId.toStdString(), + [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to read_event ({}, {})", + room_id_.toStdString(), + currentId.toStdString()); + } + }); + } +} + void TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) { diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 9b861010..10f3c490 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -162,11 +162,7 @@ public: public slots: void fetchHistory(); - void setCurrentIndex(int index) - { - currentId = indexToId(index); - emit currentIndexChanged(index); - } + void setCurrentIndex(int index); int currentIndex() const { return idToIndex(currentId); } void markEventsAsRead(const std::vector &event_ids); From a8166462adc8ffd8d6c5d2a9a50e5cde5c810588 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 4 Oct 2019 01:10:46 +0200 Subject: [PATCH 44/94] File messages (qml) --- resources/qml/TimelineView.qml | 1 + resources/qml/delegates/FileMessage.qml | 57 +++++++++++++++++++++++++ resources/res.qrc | 1 + src/timeline2/TimelineModel.cpp | 26 ++++++++++- src/timeline2/TimelineModel.h | 1 + 5 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 resources/qml/delegates/FileMessage.qml diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 35dcae03..c4750ddf 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -111,6 +111,7 @@ Rectangle { case MtxEvent.NoticeMessage: return "delegates/NoticeMessage.qml" case MtxEvent.TextMessage: return "delegates/TextMessage.qml" case MtxEvent.ImageMessage: return "delegates/ImageMessage.qml" + case MtxEvent.FileMessage: return "delegates/FileMessage.qml" //case MtxEvent.VideoMessage: return "delegates/VideoMessage.qml" case MtxEvent.Redacted: return "delegates/Redacted.qml" default: return "delegates/placeholder.qml" diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml new file mode 100644 index 00000000..3099acaa --- /dev/null +++ b/resources/qml/delegates/FileMessage.qml @@ -0,0 +1,57 @@ +import QtQuick 2.6 + +Row { +Rectangle { + radius: 10 + color: colors.dark + height: row.height + width: row.width + + Row { + id: row + + spacing: 15 + padding: 12 + + Rectangle { + color: colors.light + radius: 22 + height: 44 + width: 44 + Image { + id: img + anchors.centerIn: parent + + source: "qrc:/icons/icons/ui/arrow-pointing-down.png" + fillMode: Image.Pad + + } + MouseArea { + anchors.fill: parent + onClicked: timelineManager.saveMedia(eventData.url, eventData.filename, eventData.mimetype, eventData.type) + cursorShape: Qt.PointingHandCursor + } + } + Column { + TextEdit { + text: eventData.body + textFormat: TextEdit.PlainText + readOnly: true + wrapMode: Text.Wrap + selectByMouse: true + color: colors.text + } + TextEdit { + text: eventData.filesize + textFormat: TextEdit.PlainText + readOnly: true + wrapMode: Text.Wrap + selectByMouse: true + color: colors.text + } + } + } +} +Rectangle { +} +} diff --git a/resources/res.qrc b/resources/res.qrc index 02b4c0c0..c865200c 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -122,6 +122,7 @@ qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml + qml/delegates/FileMessage.qml qml/delegates/Redacted.qml qml/delegates/placeholder.qml diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 83d1e417..d624c938 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -87,8 +87,9 @@ eventFormattedBody(const mtx::events::RoomEvent &e) if (pos != std::string::npos) temp.erase(pos, std::string("").size()); return QString::fromStdString(temp); - } else - return QString::fromStdString(e.content.body); + } else { + return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "
"); + } } template @@ -138,6 +139,20 @@ eventFilename(const mtx::events::RoomEvent &e) return QString::fromStdString(e.content.body); } +template +auto +eventFilesize(const mtx::events::RoomEvent &e) -> decltype(e.content.info.size) +{ + return e.content.info.size; +} + +template +int64_t +eventFilesize(const T &) +{ + return 0; +} + template QString eventMimeType(const T &) @@ -341,6 +356,7 @@ TimelineModel::roleNames() const {Timestamp, "timestamp"}, {Url, "url"}, {Filename, "filename"}, + {Filesize, "filesize"}, {MimeType, "mimetype"}, {Height, "height"}, {Width, "width"}, @@ -423,6 +439,12 @@ TimelineModel::data(const QModelIndex &index, int role) const case Filename: return QVariant(boost::apply_visitor( [](const auto &e) -> QString { return eventFilename(e); }, event)); + case Filesize: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { + return utils::humanReadableFileSize(eventFilesize(e)); + }, + event)); case MimeType: return QVariant(boost::apply_visitor( [](const auto &e) -> QString { return eventMimeType(e); }, event)); diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 10f3c490..b00988f8 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -130,6 +130,7 @@ public: Timestamp, Url, Filename, + Filesize, MimeType, Height, Width, From ea98d7b2aed5d5085d0ce1833568ec93ff813b0f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 5 Oct 2019 23:11:20 +0200 Subject: [PATCH 45/94] Add simple audio message widget --- resources/qml/TimelineView.qml | 1 + resources/qml/delegates/AudioMessage.qml | 98 ++++++++++++++++++++++++ resources/qml/delegates/FileMessage.qml | 38 ++++----- resources/res.qrc | 1 + src/timeline2/TimelineViewManager.cpp | 59 ++++++++++++++ src/timeline2/TimelineViewManager.h | 2 + 6 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 resources/qml/delegates/AudioMessage.qml diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index c4750ddf..c25e6543 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -113,6 +113,7 @@ Rectangle { case MtxEvent.ImageMessage: return "delegates/ImageMessage.qml" case MtxEvent.FileMessage: return "delegates/FileMessage.qml" //case MtxEvent.VideoMessage: return "delegates/VideoMessage.qml" + case MtxEvent.AudioMessage: return "delegates/AudioMessage.qml" case MtxEvent.Redacted: return "delegates/Redacted.qml" default: return "delegates/placeholder.qml" } diff --git a/resources/qml/delegates/AudioMessage.qml b/resources/qml/delegates/AudioMessage.qml new file mode 100644 index 00000000..f36d22b9 --- /dev/null +++ b/resources/qml/delegates/AudioMessage.qml @@ -0,0 +1,98 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.6 +import QtMultimedia 5.12 + +Rectangle { + radius: 10 + color: colors.dark + height: row.height + 24 + width: parent.width + + RowLayout { + id: row + + anchors.centerIn: parent + width: parent.width - 24 + + spacing: 15 + + Rectangle { + id: button + color: colors.light + radius: 22 + height: 44 + width: 44 + Image { + id: img + anchors.centerIn: parent + + source: "qrc:/icons/icons/ui/arrow-pointing-down.png" + fillMode: Image.Pad + + } + MouseArea { + anchors.fill: parent + onClicked: { + switch (button.state) { + case "": timelineManager.cacheMedia(eventData.url, eventData.mimetype); break; + case "stopped": + audio.play(); console.log("play"); + button.state = "playing" + break + case "playing": + audio.pause(); console.log("pause"); + button.state = "stopped" + break + } + } + cursorShape: Qt.PointingHandCursor + } + MediaPlayer { + id: audio + onError: console.log(errorString) + } + + Connections { + target: timelineManager + onMediaCached: { + if (mxcUrl == eventData.url) { + audio.source = "file://" + cacheUrl + button.state = "stopped" + console.log("media loaded: " + mxcUrl + " at " + cacheUrl) + } + console.log("media cached: " + mxcUrl + " at " + cacheUrl) + } + } + + states: [ + State { + name: "stopped" + PropertyChanges { target: img; source: "qrc:/icons/icons/ui/play-sign.png" } + }, + State { + name: "playing" + PropertyChanges { target: img; source: "qrc:/icons/icons/ui/pause-symbol.png" } + } + ] + } + ColumnLayout { + id: col + + Text { + Layout.fillWidth: true + text: eventData.body + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + Text { + Layout.fillWidth: true + text: eventData.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + } + } +} + diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 3099acaa..27cd6403 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -1,19 +1,22 @@ import QtQuick 2.6 +import QtQuick.Layouts 1.6 -Row { Rectangle { radius: 10 color: colors.dark - height: row.height - width: row.width + height: row.height + 24 + width: parent.width - Row { + RowLayout { id: row + anchors.centerIn: parent + width: parent.width - 24 + spacing: 15 - padding: 12 Rectangle { + id: button color: colors.light radius: 22 height: 44 @@ -32,26 +35,23 @@ Rectangle { cursorShape: Qt.PointingHandCursor } } - Column { - TextEdit { + ColumnLayout { + id: col + + Text { + Layout.fillWidth: true text: eventData.body - textFormat: TextEdit.PlainText - readOnly: true - wrapMode: Text.Wrap - selectByMouse: true + textFormat: Text.PlainText + elide: Text.ElideRight color: colors.text } - TextEdit { + Text { + Layout.fillWidth: true text: eventData.filesize - textFormat: TextEdit.PlainText - readOnly: true - wrapMode: Text.Wrap - selectByMouse: true + textFormat: Text.PlainText + elide: Text.ElideRight color: colors.text } } } } -Rectangle { -} -} diff --git a/resources/res.qrc b/resources/res.qrc index c865200c..1caf378e 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -122,6 +122,7 @@ qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml + qml/delegates/AudioMessage.qml qml/delegates/FileMessage.qml qml/delegates/Redacted.qml qml/delegates/placeholder.qml diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 74e851c4..29c52ac9 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "Logging.h" #include "MxcImageProvider.h" @@ -143,6 +144,64 @@ TimelineViewManager::saveMedia(QString mxcUrl, }); } +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; + } + + 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()); + }); +} + void TimelineViewManager::updateReadReceipts(const QString &room_id, const std::vector &event_ids) diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index a8fcf7ce..6a6d3c6b 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -42,6 +42,7 @@ public: 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, @@ -63,6 +64,7 @@ signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); + void mediaCached(QString mxcUrl, QString cacheUrl); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); From 8a511a7862b9b6389a079afb61c9cf36c9585827 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 00:27:18 +0200 Subject: [PATCH 46/94] Add progress bar to audio messages --- resources/qml/delegates/AudioMessage.qml | 184 ++++++++++++++--------- 1 file changed, 113 insertions(+), 71 deletions(-) diff --git a/resources/qml/delegates/AudioMessage.qml b/resources/qml/delegates/AudioMessage.qml index f36d22b9..f4fa8f54 100644 --- a/resources/qml/delegates/AudioMessage.qml +++ b/resources/qml/delegates/AudioMessage.qml @@ -1,96 +1,138 @@ import QtQuick 2.6 import QtQuick.Layouts 1.6 -import QtMultimedia 5.12 +import QtQuick.Controls 2.5 +import QtMultimedia 5.6 Rectangle { radius: 10 color: colors.dark - height: row.height + 24 + height: content.height + 24 width: parent.width - RowLayout { - id: row - - anchors.centerIn: parent + ColumnLayout { + id: content width: parent.width - 24 + anchors.centerIn: parent - spacing: 15 - - Rectangle { - id: button - color: colors.light - radius: 22 - height: 44 - width: 44 - Image { - id: img - anchors.centerIn: parent - - source: "qrc:/icons/icons/ui/arrow-pointing-down.png" - fillMode: Image.Pad - + RowLayout { + Text { + id: positionText + text: "--:--:--" + color: colors.text } - MouseArea { - anchors.fill: parent - onClicked: { - switch (button.state) { - case "": timelineManager.cacheMedia(eventData.url, eventData.mimetype); break; - case "stopped": - audio.play(); console.log("play"); + Slider { + Layout.fillWidth: true + id: progress + value: media.position + from: 0 + to: media.duration + + onMoved: media.seek(value) + //indeterminate: true + function updatePositionTexts() { + function formatTime(date) { + var hh = date.getUTCHours(); + var mm = date.getUTCMinutes(); + var ss = date.getSeconds(); + if (hh < 10) {hh = "0"+hh;} + if (mm < 10) {mm = "0"+mm;} + if (ss < 10) {ss = "0"+ss;} + return hh+":"+mm+":"+ss; + } + positionText.text = formatTime(new Date(media.position)) + durationText.text = formatTime(new Date(media.duration)) + } + onValueChanged: updatePositionTexts() + } + Text { + id: durationText + text: "--:--:--" + color: colors.text + } + } + + RowLayout { + width: parent.width + + spacing: 15 + + Rectangle { + id: button + color: colors.light + radius: 22 + height: 44 + width: 44 + Image { + id: img + anchors.centerIn: parent + + source: "qrc:/icons/icons/ui/arrow-pointing-down.png" + fillMode: Image.Pad + + } + MouseArea { + anchors.fill: parent + onClicked: { + switch (button.state) { + case "": timelineManager.cacheMedia(eventData.url, eventData.mimetype); break; + case "stopped": + media.play(); console.log("play"); button.state = "playing" break - case "playing": - audio.pause(); console.log("pause"); + case "playing": + media.pause(); console.log("pause"); button.state = "stopped" break + } + } + cursorShape: Qt.PointingHandCursor + } + MediaPlayer { + id: media + onError: console.log(errorString) + onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts() + } + + Connections { + target: timelineManager + onMediaCached: { + if (mxcUrl == eventData.url) { + media.source = "file://" + cacheUrl + button.state = "stopped" + console.log("media loaded: " + mxcUrl + " at " + cacheUrl) + } + console.log("media cached: " + mxcUrl + " at " + cacheUrl) } } - cursorShape: Qt.PointingHandCursor - } - MediaPlayer { - id: audio - onError: console.log(errorString) - } - Connections { - target: timelineManager - onMediaCached: { - if (mxcUrl == eventData.url) { - audio.source = "file://" + cacheUrl - button.state = "stopped" - console.log("media loaded: " + mxcUrl + " at " + cacheUrl) + states: [ + State { + name: "stopped" + PropertyChanges { target: img; source: "qrc:/icons/icons/ui/play-sign.png" } + }, + State { + name: "playing" + PropertyChanges { target: img; source: "qrc:/icons/icons/ui/pause-symbol.png" } } - console.log("media cached: " + mxcUrl + " at " + cacheUrl) - } + ] } + ColumnLayout { + id: col - states: [ - State { - name: "stopped" - PropertyChanges { target: img; source: "qrc:/icons/icons/ui/play-sign.png" } - }, - State { - name: "playing" - PropertyChanges { target: img; source: "qrc:/icons/icons/ui/pause-symbol.png" } + Text { + Layout.fillWidth: true + text: eventData.body + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + Text { + Layout.fillWidth: true + text: eventData.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text } - ] - } - ColumnLayout { - id: col - - Text { - Layout.fillWidth: true - text: eventData.body - textFormat: Text.PlainText - elide: Text.ElideRight - color: colors.text - } - Text { - Layout.fillWidth: true - text: eventData.filesize - textFormat: Text.PlainText - elide: Text.ElideRight - color: colors.text } } } From 67d255a2de7ca85b35a67bff25ef9b6770b453a0 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 01:05:32 +0200 Subject: [PATCH 47/94] Add basic video messages Size is fixed for now, otherwise the Video output ends up somewhere... --- resources/qml/TimelineView.qml | 4 ++-- .../{AudioMessage.qml => PlayableMediaMessage.qml} | 12 ++++++++++++ resources/res.qrc | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) rename resources/qml/delegates/{AudioMessage.qml => PlayableMediaMessage.qml} (91%) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index c25e6543..4d8c22b8 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -112,8 +112,8 @@ Rectangle { case MtxEvent.TextMessage: return "delegates/TextMessage.qml" case MtxEvent.ImageMessage: return "delegates/ImageMessage.qml" case MtxEvent.FileMessage: return "delegates/FileMessage.qml" - //case MtxEvent.VideoMessage: return "delegates/VideoMessage.qml" - case MtxEvent.AudioMessage: return "delegates/AudioMessage.qml" + case MtxEvent.VideoMessage: return "delegates/PlayableMediaMessage.qml" + case MtxEvent.AudioMessage: return "delegates/PlayableMediaMessage.qml" case MtxEvent.Redacted: return "delegates/Redacted.qml" default: return "delegates/placeholder.qml" } diff --git a/resources/qml/delegates/AudioMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml similarity index 91% rename from resources/qml/delegates/AudioMessage.qml rename to resources/qml/delegates/PlayableMediaMessage.qml index f4fa8f54..7498d5b2 100644 --- a/resources/qml/delegates/AudioMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -3,6 +3,8 @@ import QtQuick.Layouts 1.6 import QtQuick.Controls 2.5 import QtMultimedia 5.6 +import com.github.nheko 1.0 + Rectangle { radius: 10 color: colors.dark @@ -14,6 +16,15 @@ Rectangle { width: parent.width - 24 anchors.centerIn: parent + VideoOutput { + visible: eventData.type == MtxEvent.VideoMessage + Layout.maximumHeight: 300 + Layout.minimumHeight: 300 + Layout.maximumWidth: 500 + fillMode: VideoOutput.PreserveAspectFit + source: media + } + RowLayout { Text { id: positionText @@ -91,6 +102,7 @@ Rectangle { id: media onError: console.log(errorString) onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts() + autoPlay: true } Connections { diff --git a/resources/res.qrc b/resources/res.qrc index 1caf378e..16bab4e4 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -122,7 +122,7 @@ qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml - qml/delegates/AudioMessage.qml + qml/delegates/PlayableMediaMessage.qml qml/delegates/FileMessage.qml qml/delegates/Redacted.qml qml/delegates/placeholder.qml From 425d534e225a956fefb3bc2e8d2c8d26152f6353 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 01:44:02 +0200 Subject: [PATCH 48/94] Enable Sticker and Emote messages --- resources/qml/TimelineView.qml | 4 +++- resources/qml/delegates/ImageMessage.qml | 3 +++ src/timeline2/TimelineModel.cpp | 16 ++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 4d8c22b8..b641992d 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -110,7 +110,9 @@ Rectangle { //case MtxEvent.Topic: return "delegates/Topic.qml" case MtxEvent.NoticeMessage: return "delegates/NoticeMessage.qml" case MtxEvent.TextMessage: return "delegates/TextMessage.qml" + case MtxEvent.EmoteMessage: return "delegates/TextMessage.qml" case MtxEvent.ImageMessage: return "delegates/ImageMessage.qml" + case MtxEvent.Sticker: return "delegates/ImageMessage.qml" case MtxEvent.FileMessage: return "delegates/FileMessage.qml" case MtxEvent.VideoMessage: return "delegates/PlayableMediaMessage.qml" case MtxEvent.AudioMessage: return "delegates/PlayableMediaMessage.qml" @@ -204,7 +206,7 @@ Rectangle { onTriggered: chat.model.redactEvent(model.id) } MenuItem { - visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage + 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) } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 3f5c00bf..2ed41a17 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -1,5 +1,7 @@ import QtQuick 2.6 +import com.github.nheko 1.0 + Item { width: 300 height: 300 * eventData.proportionalHeight @@ -13,6 +15,7 @@ Item { fillMode: Image.PreserveAspectFit MouseArea { + enabled: eventData.type == MtxEvent.ImageMessage anchors.fill: parent onClicked: timelineManager.openImageOverlay(eventData.url, eventData.filename, eventData.mimetype, eventData.type) } diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index d624c938..2d1a79c2 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -16,26 +16,26 @@ namespace { template QString -eventId(const T &event) +eventId(const mtx::events::RoomEvent &event) { return QString::fromStdString(event.event_id); } template QString -roomId(const T &event) +roomId(const mtx::events::Event &event) { return QString::fromStdString(event.room_id); } template QString -senderId(const T &event) +senderId(const mtx::events::RoomEvent &event) { return QString::fromStdString(event.sender); } template QDateTime -eventTimestamp(const T &event) +eventTimestamp(const mtx::events::RoomEvent &event) { return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); } @@ -94,7 +94,7 @@ eventFormattedBody(const mtx::events::RoomEvent &e) template QString -eventUrl(const T &) +eventUrl(const mtx::events::Event &) { return ""; } @@ -108,7 +108,7 @@ eventUrl(const mtx::events::RoomEvent &e) template QString -eventFilename(const T &) +eventFilename(const mtx::events::Event &) { return ""; } @@ -148,14 +148,14 @@ eventFilesize(const mtx::events::RoomEvent &e) -> decltype(e.content.info.siz template int64_t -eventFilesize(const T &) +eventFilesize(const mtx::events::Event &) { return 0; } template QString -eventMimeType(const T &) +eventMimeType(const mtx::events::Event &) { return QString(); } From a1919d00d01504fa1d6ff21aa676a4276ffb8792 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 02:28:25 +0200 Subject: [PATCH 49/94] Try to package qml modules --- .ci/linux/deploy.sh | 3 +-- .ci/macos/deploy.sh | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh index 2caf5e0f..524d72d5 100755 --- a/.ci/linux/deploy.sh +++ b/.ci/linux/deploy.sh @@ -44,8 +44,7 @@ do linuxdeployqt=$res done -./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -bundle-non-qt-libs -./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -appimage +./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -bundle-non-qt-libs -qmldir=./resources/qml -appimage chmod +x nheko-*x86_64.AppImage diff --git a/.ci/macos/deploy.sh b/.ci/macos/deploy.sh index ee4acaed..1dc9472d 100755 --- a/.ci/macos/deploy.sh +++ b/.ci/macos/deploy.sh @@ -16,7 +16,7 @@ PATH=/usr/local/opt/qt/bin/:${PATH} mkdir -p nheko.app/Contents/Frameworks find "${ICU_LIB}" -type l -name "*.dylib" -exec cp -a -n {} nheko.app/Contents/Frameworks/ \; || true - sudo macdeployqt nheko.app -dmg -always-overwrite + sudo macdeployqt nheko.app -dmg -always-overwrite -qmldir=../resources/qml/ user=$(id -nu) sudo chown "${user}" nheko.dmg From e19645042891681231eb31554ae37a9262731118 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 11:08:13 +0200 Subject: [PATCH 50/94] Use QtQuick.Controls compatible with Qt 5.8 --- resources/qml/EncryptionIndicator.qml | 2 +- resources/qml/StatusIndicator.qml | 2 +- resources/qml/TimelineView.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 2 +- resources/qml/delegates/Redacted.qml | 2 +- resources/qml/delegates/placeholder.qml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index 6cb5138a..0d0e86cf 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 2.5 +import QtQuick.Controls 2.1 import QtGraphicalEffects 1.0 import com.github.nheko 1.0 diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 440a7e47..bc28456f 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 2.5 +import QtQuick.Controls 2.1 import QtGraphicalEffects 1.0 import com.github.nheko 1.0 diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index b641992d..65933124 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -1,5 +1,5 @@ import QtQuick 2.6 -import QtQuick.Controls 2.5 +import QtQuick.Controls 2.1 import QtQuick.Layouts 1.5 import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 7498d5b2..645ca102 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -1,6 +1,6 @@ import QtQuick 2.6 import QtQuick.Layouts 1.6 -import QtQuick.Controls 2.5 +import QtQuick.Controls 2.1 import QtMultimedia 5.6 import com.github.nheko 1.0 diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml index 53e95a83..42fb4835 100644 --- a/resources/qml/delegates/Redacted.qml +++ b/resources/qml/delegates/Redacted.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 2.5 +import QtQuick.Controls 2.1 Label { text: qsTr("redacted") diff --git a/resources/qml/delegates/placeholder.qml b/resources/qml/delegates/placeholder.qml index d17184f3..e64bc368 100644 --- a/resources/qml/delegates/placeholder.qml +++ b/resources/qml/delegates/placeholder.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 2.5 +import QtQuick.Controls 2.1 Label { text: qsTr("unimplemented event: ") + eventData.type From 489165d579d6b32d902427a5744971d97ea5be03 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 12:12:29 +0200 Subject: [PATCH 51/94] Lower requirement on QtQuick.Layouts version --- resources/qml/TimelineView.qml | 2 +- resources/qml/delegates/FileMessage.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 65933124..4782c1f1 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -1,6 +1,6 @@ import QtQuick 2.6 import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.5 +import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 27cd6403..6dd552ab 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -1,5 +1,5 @@ import QtQuick 2.6 -import QtQuick.Layouts 1.6 +import QtQuick.Layouts 1.2 Rectangle { radius: 10 diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 645ca102..5a5a2162 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -1,5 +1,5 @@ import QtQuick 2.6 -import QtQuick.Layouts 1.6 +import QtQuick.Layouts 1.2 import QtQuick.Controls 2.1 import QtMultimedia 5.6 From 55696ec0ba1b3bb47f1aeb5b8b05ab66b9a9a868 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 12:59:22 +0200 Subject: [PATCH 52/94] Add required graphicaleffects package --- .ci/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/install.sh b/.ci/install.sh index b2fa5cda..0942af62 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -55,5 +55,6 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then qt${QT_PKG}svg \ qt${QT_PKG}multimedia \ qt${QT_PKG}quickcontrols2 \ + qt${QT_PKG}graphicaleffects \ liblmdb-dev fi From 241c0236fc2b98033bb4d9468cec82c21016b009 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 12:59:51 +0200 Subject: [PATCH 53/94] Try to fix windows Winsock.h compilation error --- src/timeline2/TimelineModel.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index b00988f8..16b0af7f 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -8,6 +7,8 @@ #include #include +#include + #include "Cache.h" #include "Logging.h" #include "MatrixClient.h" From ec6953d0c09292b223147c299f893325e50fbf3b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 14:00:49 +0200 Subject: [PATCH 54/94] Fix linting issues --- src/timeline2/TimelineModel.cpp | 2 +- src/timeline2/TimelineModel.h | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 2d1a79c2..b3ddf899 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -610,7 +610,7 @@ void TimelineModel::setCurrentIndex(int index) { auto oldIndex = idToIndex(currentId); - currentId = indexToId(index); + currentId = indexToId(index); emit currentIndexChanged(index); if (oldIndex < index) { diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 16b0af7f..35ec325d 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -1,6 +1,5 @@ #pragma once - #include #include #include From 084396059b97e6c646cfe3e1cf7816fa47dd6cfb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 15:05:12 +0200 Subject: [PATCH 55/94] Use win lean and mean to fix WinSock include issue --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cf34c32..f659d91c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -454,6 +454,7 @@ if(APPLE) target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras) elseif(WIN32) add_executable (nheko ${OS_BUNDLE} ${ICON_FILE} ${NHEKO_DEPS}) + target_compile_definitions(nheko PRIVATE WIN32_LEAN_AND_MEAN) target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain) else() add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) From e828d9ed7b4ddd2a5956722a5862715453739b7c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 15:26:02 +0200 Subject: [PATCH 56/94] Disable autoplayback again --- resources/qml/StatusIndicator.qml | 1 + resources/qml/delegates/PlayableMediaMessage.qml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index bc28456f..9f8d2cae 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -39,6 +39,7 @@ Rectangle { anchors.fill: stateImg source: stateImg color: colors.buttonText + visible: stateImg.source != "" } } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 5a5a2162..2385c750 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -102,7 +102,6 @@ Rectangle { id: media onError: console.log(errorString) onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts() - autoPlay: true } Connections { From 7f4175216538fab6d08a0d44b280a3a70f465734 Mon Sep 17 00:00:00 2001 From: Benedikt Bongartz Date: Mon, 7 Oct 2019 23:36:33 +0200 Subject: [PATCH 57/94] fix: add missing quickcontrols2 dep --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2e01b40b..dddd1c6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN \ add-apt-repository -y ppa:ubuntu-toolchain-r/test && \ apt-get update -qq && \ apt-get install -y \ - qt510base qt510tools qt510svg qt510multimedia \ + qt510base qt510tools qt510svg qt510multimedia qt510quickcontrols2 qt510graphicaleffects \ gcc-5 g++-5 RUN \ @@ -44,4 +44,4 @@ ENV PATH=/opt/qt510/bin:$PATH RUN mkdir /build -WORKDIR /build \ No newline at end of file +WORKDIR /build From b9076c5c4d1beb7ff4cb4a2db8e6eb4e7f5b0dcd Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 8 Oct 2019 20:55:09 +0200 Subject: [PATCH 58/94] Try out DelegateChooser requires Qt5.12+ --- resources/qml/TimelineView.qml | 176 ++++-------------- resources/qml/delegates/FileMessage.qml | 6 +- resources/qml/delegates/ImageMessage.qml | 8 +- resources/qml/delegates/NoticeMessage.qml | 2 +- .../qml/delegates/PlayableMediaMessage.qml | 10 +- resources/qml/delegates/TextMessage.qml | 2 +- resources/qml/delegates/TimelineRow.qml | 139 ++++++++++++++ resources/qml/delegates/placeholder.qml | 2 +- resources/res.qrc | 1 + 9 files changed, 189 insertions(+), 157 deletions(-) create mode 100644 resources/qml/delegates/TimelineRow.qml diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 4782c1f1..0642b13a 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -3,9 +3,12 @@ import QtQuick.Controls 2.1 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 +import Qt.labs.qmlmodels 1.0 import com.github.nheko 1.0 +import "./delegates" + Rectangle { anchors.fill: parent @@ -77,158 +80,47 @@ Rectangle { onMovementEnded: updatePosition() spacing: 4 - delegate: RowLayout { - anchors.leftMargin: avatarSize + 4 - anchors.left: parent.left - anchors.right: parent.right - anchors.rightMargin: scrollbar.width - - function isFullyVisible() { - return (y - chat.contentY - 1) + height < chat.height + delegate: DelegateChooser { + role: "type" + DelegateChoice { + roleValue: MtxEvent.TextMessage + TimelineRow { view: chat; TextMessage { id: kid } } } - function getIndex() { - return index; + DelegateChoice { + roleValue: MtxEvent.NoticeMessage + TimelineRow { view: chat; NoticeMessage { id: kid } } } - - Loader { - id: loader - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - height: item.height - - source: switch(model.type) { - //case MtxEvent.Aliases: return "delegates/Aliases.qml" - //case MtxEvent.Avatar: return "delegates/Avatar.qml" - //case MtxEvent.CanonicalAlias: return "delegates/CanonicalAlias.qml" - //case MtxEvent.Create: return "delegates/Create.qml" - //case MtxEvent.GuestAccess: return "delegates/GuestAccess.qml" - //case MtxEvent.HistoryVisibility: return "delegates/HistoryVisibility.qml" - //case MtxEvent.JoinRules: return "delegates/JoinRules.qml" - //case MtxEvent.Member: return "delegates/Member.qml" - //case MtxEvent.Name: return "delegates/Name.qml" - //case MtxEvent.PowerLevels: return "delegates/PowerLevels.qml" - //case MtxEvent.Topic: return "delegates/Topic.qml" - case MtxEvent.NoticeMessage: return "delegates/NoticeMessage.qml" - case MtxEvent.TextMessage: return "delegates/TextMessage.qml" - case MtxEvent.EmoteMessage: return "delegates/TextMessage.qml" - case MtxEvent.ImageMessage: return "delegates/ImageMessage.qml" - case MtxEvent.Sticker: return "delegates/ImageMessage.qml" - case MtxEvent.FileMessage: return "delegates/FileMessage.qml" - case MtxEvent.VideoMessage: return "delegates/PlayableMediaMessage.qml" - case MtxEvent.AudioMessage: return "delegates/PlayableMediaMessage.qml" - case MtxEvent.Redacted: return "delegates/Redacted.qml" - default: return "delegates/placeholder.qml" - } - property variant eventData: model + DelegateChoice { + roleValue: MtxEvent.EmoteMessage + TimelineRow { view: chat; TextMessage { id: kid } } } - - StatusIndicator { - state: model.state - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 + DelegateChoice { + roleValue: MtxEvent.ImageMessage + TimelineRow { view: chat; ImageMessage { id: kid } } } - - EncryptionIndicator { - visible: model.isEncrypted - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 + DelegateChoice { + roleValue: MtxEvent.Sticker + TimelineRow { view: chat; ImageMessage { id: kid } } } - - Button { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - id: replyButton - flat: true - Layout.preferredHeight: 16 - ToolTip.visible: hovered - ToolTip.text: qsTr("Reply") - - // disable background, because we don't want a border on hover - background: Item { - } - - Image { - id: replyButtonImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: "qrc:/icons/icons/ui/mail-reply.png" - } - ColorOverlay { - anchors.fill: replyButtonImg - source: replyButtonImg - color: replyButton.hovered ? colors.highlight : colors.buttonText - } - - onClicked: chat.model.replyAction(model.id) + DelegateChoice { + roleValue: MtxEvent.FileMessage + TimelineRow { view: chat; FileMessage { id: kid } } } - Button { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - id: optionsButton - flat: true - Layout.preferredHeight: 16 - ToolTip.visible: hovered - ToolTip.text: qsTr("Options") - - // disable background, because we don't want a border on hover - background: Item { - } - - Image { - id: optionsButtonImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: "qrc:/icons/icons/ui/vertical-ellipsis.png" - } - ColorOverlay { - anchors.fill: optionsButtonImg - source: optionsButtonImg - color: optionsButton.hovered ? colors.highlight : colors.buttonText - } - - onClicked: contextMenu.open() - - Menu { - y: optionsButton.height - id: contextMenu - - MenuItem { - text: qsTr("Read receipts") - onTriggered: chat.model.readReceiptsAction(model.id) - } - MenuItem { - text: qsTr("Mark as read") - } - MenuItem { - text: qsTr("View raw message") - onTriggered: chat.model.viewRawMessage(model.id) - } - MenuItem { - text: qsTr("Redact message") - onTriggered: chat.model.redactEvent(model.id) - } - 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) - } - } + DelegateChoice { + roleValue: MtxEvent.VideoMessage + TimelineRow { view: chat; PlayableMediaMessage { id: kid } } } - - Text { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - text: model.timestamp.toLocaleTimeString("HH:mm") - color: inactiveColors.text - - ToolTip.visible: ma.containsMouse - ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) - - MouseArea{ - id: ma - anchors.fill: parent - hoverEnabled: true - } + DelegateChoice { + roleValue: MtxEvent.AudioMessage + TimelineRow { view: chat; PlayableMediaMessage { id: kid } } + } + DelegateChoice { + roleValue: MtxEvent.Redacted + TimelineRow { view: chat; Redacted { id: kid } } } } + section { property: "section" delegate: Column { diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 6dd552ab..ad2c695d 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(eventData.url, eventData.filename, eventData.mimetype, eventData.type) + onClicked: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) cursorShape: Qt.PointingHandCursor } } @@ -40,14 +40,14 @@ Rectangle { Text { Layout.fillWidth: true - text: eventData.body + text: model.body textFormat: Text.PlainText elide: Text.ElideRight color: colors.text } Text { Layout.fillWidth: true - text: eventData.filesize + text: model.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 2ed41a17..70d2debe 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -4,20 +4,20 @@ import com.github.nheko 1.0 Item { width: 300 - height: 300 * eventData.proportionalHeight + height: 300 * model.proportionalHeight Image { id: img anchors.fill: parent - source: eventData.url.replace("mxc://", "image://MxcImage/") + source: model.url.replace("mxc://", "image://MxcImage/") asynchronous: true fillMode: Image.PreserveAspectFit MouseArea { - enabled: eventData.type == MtxEvent.ImageMessage + enabled: model.type == MtxEvent.ImageMessage anchors.fill: parent - onClicked: timelineManager.openImageOverlay(eventData.url, eventData.filename, eventData.mimetype, eventData.type) + onClicked: timelineManager.openImageOverlay(model.url, model.filename, model.mimetype, model.type) } } } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index 5f04d235..b916d65a 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -1,7 +1,7 @@ import QtQuick 2.5 TextEdit { - text: eventData.formattedBody + text: model.formattedBody textFormat: TextEdit.RichText readOnly: true wrapMode: Text.Wrap diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 2385c750..c716d21d 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -17,7 +17,7 @@ Rectangle { anchors.centerIn: parent VideoOutput { - visible: eventData.type == MtxEvent.VideoMessage + visible: model.type == MtxEvent.VideoMessage Layout.maximumHeight: 300 Layout.minimumHeight: 300 Layout.maximumWidth: 500 @@ -85,7 +85,7 @@ Rectangle { anchors.fill: parent onClicked: { switch (button.state) { - case "": timelineManager.cacheMedia(eventData.url, eventData.mimetype); break; + case "": timelineManager.cacheMedia(model.url, model.mimetype); break; case "stopped": media.play(); console.log("play"); button.state = "playing" @@ -107,7 +107,7 @@ Rectangle { Connections { target: timelineManager onMediaCached: { - if (mxcUrl == eventData.url) { + if (mxcUrl == model.url) { media.source = "file://" + cacheUrl button.state = "stopped" console.log("media loaded: " + mxcUrl + " at " + cacheUrl) @@ -132,14 +132,14 @@ Rectangle { Text { Layout.fillWidth: true - text: eventData.body + text: model.body textFormat: Text.PlainText elide: Text.ElideRight color: colors.text } Text { Layout.fillWidth: true - text: eventData.filesize + text: model.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 f7dba618..3a3492ed 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -1,7 +1,7 @@ import QtQuick 2.5 TextEdit { - text: eventData.formattedBody + text: model.formattedBody textFormat: TextEdit.RichText readOnly: true wrapMode: Text.Wrap diff --git a/resources/qml/delegates/TimelineRow.qml b/resources/qml/delegates/TimelineRow.qml new file mode 100644 index 00000000..28a2ec8c --- /dev/null +++ b/resources/qml/delegates/TimelineRow.qml @@ -0,0 +1,139 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtQuick.Window 2.2 + +import com.github.nheko 1.0 + +import ".." + +RowLayout { + property var view: undefined + default property alias data: contentItem.data + + height: kid.height // TODO: fix this, we shouldn't need to give the child of contentItem this id! + anchors.leftMargin: avatarSize + 4 + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: scrollbar.width + + function isFullyVisible() { + return (y - view.contentY - 1) + height < view.height + } + function getIndex() { + return index; + } + + Item { + id: contentItem + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + } + + StatusIndicator { + state: model.state + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } + + EncryptionIndicator { + visible: model.isEncrypted + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } + + Button { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + id: replyButton + flat: true + Layout.preferredHeight: 16 + ToolTip.visible: hovered + ToolTip.text: qsTr("Reply") + + // disable background, because we don't want a border on hover + background: Item { + } + + Image { + id: replyButtonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "qrc:/icons/icons/ui/mail-reply.png" + } + ColorOverlay { + anchors.fill: replyButtonImg + source: replyButtonImg + color: replyButton.hovered ? colors.highlight : colors.buttonText + } + + onClicked: view.model.replyAction(model.id) + } + Button { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + id: optionsButton + flat: true + Layout.preferredHeight: 16 + ToolTip.visible: hovered + ToolTip.text: qsTr("Options") + + // disable background, because we don't want a border on hover + background: Item { + } + + Image { + id: optionsButtonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "qrc:/icons/icons/ui/vertical-ellipsis.png" + } + ColorOverlay { + anchors.fill: optionsButtonImg + source: optionsButtonImg + color: optionsButton.hovered ? colors.highlight : colors.buttonText + } + + onClicked: contextMenu.open() + + Menu { + y: optionsButton.height + id: contextMenu + + MenuItem { + text: qsTr("Read receipts") + onTriggered: view.model.readReceiptsAction(model.id) + } + MenuItem { + text: qsTr("Mark as read") + } + MenuItem { + text: qsTr("View raw message") + onTriggered: view.model.viewRawMessage(model.id) + } + MenuItem { + text: qsTr("Redact message") + onTriggered: view.model.redactEvent(model.id) + } + 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) + } + } + } + + Text { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + text: model.timestamp.toLocaleTimeString("HH:mm") + color: inactiveColors.text + + ToolTip.visible: ma.containsMouse + ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) + + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + } +} diff --git a/resources/qml/delegates/placeholder.qml b/resources/qml/delegates/placeholder.qml index e64bc368..462af2db 100644 --- a/resources/qml/delegates/placeholder.qml +++ b/resources/qml/delegates/placeholder.qml @@ -2,7 +2,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 Label { - text: qsTr("unimplemented event: ") + eventData.type + text: qsTr("unimplemented event: ") + model.type textFormat: Text.PlainText wrapMode: Text.Wrap width: parent.width diff --git a/resources/res.qrc b/resources/res.qrc index 16bab4e4..2e0f89ce 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -119,6 +119,7 @@ qml/Avatar.qml qml/StatusIndicator.qml qml/EncryptionIndicator.qml + qml/delegates/TimelineRow.qml qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml From d90038cf20fb6ba9c9d47c2f6fe689de71ea89a7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 8 Oct 2019 21:26:52 +0200 Subject: [PATCH 59/94] Misc layout fixes --- resources/qml/TimelineView.qml | 6 ++++++ .../qml/delegates/{placeholder.qml => Placeholder.qml} | 0 resources/res.qrc | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) rename resources/qml/delegates/{placeholder.qml => Placeholder.qml} (100%) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 0642b13a..a758db9a 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -118,6 +118,10 @@ Rectangle { roleValue: MtxEvent.Redacted TimelineRow { view: chat; Redacted { id: kid } } } + DelegateChoice { + //roleValue: MtxEvent.Redacted + TimelineRow { view: chat; Placeholder { id: kid } } + } } @@ -130,6 +134,8 @@ Rectangle { width: parent.width + Component.onCompleted: chat.forceLayout() + Label { id: dateBubble anchors.horizontalCenter: parent.horizontalCenter diff --git a/resources/qml/delegates/placeholder.qml b/resources/qml/delegates/Placeholder.qml similarity index 100% rename from resources/qml/delegates/placeholder.qml rename to resources/qml/delegates/Placeholder.qml diff --git a/resources/res.qrc b/resources/res.qrc index 2e0f89ce..11a20e54 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -126,6 +126,6 @@ qml/delegates/PlayableMediaMessage.qml qml/delegates/FileMessage.qml qml/delegates/Redacted.qml - qml/delegates/placeholder.qml + qml/delegates/Placeholder.qml From 8ebef4eed2134179e5609104eb72fe8f055a35f1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 9 Oct 2019 00:36:03 +0200 Subject: [PATCH 60/94] Size images/videos by timeline width --- resources/qml/delegates/ImageMessage.qml | 4 +-- .../qml/delegates/PlayableMediaMessage.qml | 27 ++++++++++++++----- src/AvatarProvider.cpp | 4 +-- src/MxcImageProvider.cpp | 3 ++- src/timeline2/TimelineModel.cpp | 19 +++++++++++++ src/timeline2/TimelineModel.h | 1 + 6 files changed, 46 insertions(+), 12 deletions(-) diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 70d2debe..f1e95e3d 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -3,8 +3,8 @@ import QtQuick 2.6 import com.github.nheko 1.0 Item { - width: 300 - height: 300 * model.proportionalHeight + width: Math.min(parent.width, model.width) + height: width * model.proportionalHeight Image { id: img diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index c716d21d..3a518617 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -6,26 +6,38 @@ import QtMultimedia 5.6 import com.github.nheko 1.0 Rectangle { + id: bg radius: 10 color: colors.dark height: content.height + 24 width: parent.width - ColumnLayout { + Column { id: content width: parent.width - 24 anchors.centerIn: parent - VideoOutput { + Rectangle { + id: videoContainer visible: model.type == MtxEvent.VideoMessage - Layout.maximumHeight: 300 - Layout.minimumHeight: 300 - Layout.maximumWidth: 500 - fillMode: VideoOutput.PreserveAspectFit - source: media + width: Math.min(parent.width, model.width) + height: width*model.proportionalHeight + Image { + anchors.fill: parent + source: model.thumbnailUrl.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit + + VideoOutput { + anchors.fill: parent + fillMode: VideoOutput.PreserveAspectFit + source: media + } + } } RowLayout { + width: parent.width Text { id: positionText text: "--:--:--" @@ -102,6 +114,7 @@ Rectangle { id: media onError: console.log(errorString) onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts() + onStopped: button.state = "stopped" } Connections { diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp index c83ffe0f..68b6901e 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp @@ -67,8 +67,8 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca }); mtx::http::ThumbOpts opts; - opts.width = 256; - opts.height = 256; + opts.width = size; + opts.height = size; opts.mxc_url = avatarUrl.toStdString(); http::client()->get_thumbnail( diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index 305439fc..86dbcabc 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -38,7 +38,8 @@ MxcImageResponse::run() auto data = QByteArray(res.data(), res.size()); cache::client()->saveImage(fileName, data); m_image.loadFromData(data); - m_image = m_image.scaled(m_requestedSize, Qt::KeepAspectRatio); + m_image = m_image.scaled( + m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); m_image.setText("mxc url", "mxc://" + m_id); emit finished(); diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index b3ddf899..27bd09b6 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -106,6 +106,21 @@ eventUrl(const mtx::events::RoomEvent &e) return QString::fromStdString(e.content.url); } +template +QString +eventThumbnailUrl(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventThumbnailUrl(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, + QString> +{ + return QString::fromStdString(e.content.info.thumbnail_url); +} + template QString eventFilename(const mtx::events::Event &) @@ -355,6 +370,7 @@ TimelineModel::roleNames() const {UserName, "userName"}, {Timestamp, "timestamp"}, {Url, "url"}, + {ThumbnailUrl, "thumbnailUrl"}, {Filename, "filename"}, {Filesize, "filesize"}, {MimeType, "mimetype"}, @@ -436,6 +452,9 @@ TimelineModel::data(const QModelIndex &index, int role) const case Url: return QVariant(boost::apply_visitor( [](const auto &e) -> QString { return eventUrl(e); }, event)); + case ThumbnailUrl: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventThumbnailUrl(e); }, event)); case Filename: return QVariant(boost::apply_visitor( [](const auto &e) -> QString { return eventFilename(e); }, event)); diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 35ec325d..b7ff546b 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -129,6 +129,7 @@ public: UserName, Timestamp, Url, + ThumbnailUrl, Filename, Filesize, MimeType, From 0fd2199112be88a17125ea5630ce6184eb0758ca Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 9 Oct 2019 19:34:57 +0200 Subject: [PATCH 61/94] Load content if no scrollbar is needed --- resources/qml/TimelineView.qml | 10 ++++++++++ src/timeline2/TimelineViewManager.cpp | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index a758db9a..ef1db0f0 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -44,6 +44,10 @@ Rectangle { } else { positionViewAtIndex(model.currentIndex, ListView.End) } + + if (contentHeight < height) { + model.fetchHistory(); + } } } @@ -63,8 +67,14 @@ Rectangle { currentIndex = newIndex model.currentIndex = newIndex } + + if (contentHeight < height) { + model.fetchHistory(); + } } + onAtYBeginningChanged: if (atYBeginning) model.fetchHistory() + function updatePosition() { for (var y = chat.contentY + chat.height; y > chat.height; y -= 5) { var i = chat.itemAt(100, y); diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 29c52ac9..13025864 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -53,7 +53,6 @@ TimelineViewManager::setHistoryView(const QString &room_id) auto room = models.find(room_id); if (room != models.end()) { timeline_ = room.value().data(); - timeline_->fetchHistory(); emit activeTimelineChanged(timeline_); nhlog::ui()->info("Activated room {}", room_id.toStdString()); } From a83ae7e95fcb21f0f61cdb7d8cf9e4c4985bd853 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 13 Oct 2019 15:10:33 +0200 Subject: [PATCH 62/94] Fix section layout issues and pagination issues Pagination could get stuck, if the messages request failed. Section height seemes to have been calculated to late, which would make some section overlap the next message in some cases. Fix that by doing the height calculation manually. --- resources/qml/TimelineView.qml | 5 ++--- src/dialogs/ImageOverlay.cpp | 1 - src/timeline2/TimelineModel.cpp | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index ef1db0f0..b0a8853e 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -68,7 +68,7 @@ Rectangle { model.currentIndex = newIndex } - if (contentHeight < height) { + if (contentHeight < height && model) { model.fetchHistory(); } } @@ -143,8 +143,7 @@ Rectangle { spacing: 8 width: parent.width - - Component.onCompleted: chat.forceLayout() + height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 Label { id: dateBubble diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp index dd9cd03a..cbdd351c 100644 --- a/src/dialogs/ImageOverlay.cpp +++ b/src/dialogs/ImageOverlay.cpp @@ -41,7 +41,6 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent) setAttribute(Qt::WA_DeleteOnClose, true); setWindowState(Qt::WindowFullScreen); - // Deprecated in 5.13: screen_ = QApplication::desktop()->availableGeometry(); screen_ = QGuiApplication::primaryScreen()->availableGeometry(); move(QApplication::desktop()->mapToGlobal(screen_.topLeft())); diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 27bd09b6..b37ade54 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -618,10 +618,12 @@ TimelineModel::fetchHistory() 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; }); } @@ -658,8 +660,6 @@ TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) } prev_batch_token_ = QString::fromStdString(msgs.end); - - paginationInProgress = false; } QColor From cff46d97a8636f41dd5ac2ace9dc00ecb5f4c51c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 17 Oct 2019 09:36:16 +0200 Subject: [PATCH 63/94] Add native themeing to QML (where possible) --- resources/qml/TimelineView.qml | 5 +-- resources/qml/delegates/TimelineRow.qml | 28 +++++++++++----- src/Utils.cpp | 36 ++++++++++++-------- src/timeline2/TimelineViewManager.cpp | 44 +++++++++++++++++++++++++ src/timeline2/TimelineViewManager.h | 1 + 5 files changed, 91 insertions(+), 23 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index b0a8853e..d1ada3ea 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -12,8 +12,9 @@ import "./delegates" Rectangle { anchors.fill: parent - SystemPalette { id: colors; colorGroup: SystemPalette.Active } - SystemPalette { id: inactiveColors; colorGroup: SystemPalette.Disabled } + property var colors: currentActivePalette + property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled } + property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive property int avatarSize: 32 color: colors.window diff --git a/resources/qml/delegates/TimelineRow.qml b/resources/qml/delegates/TimelineRow.qml index 28a2ec8c..3019deb1 100644 --- a/resources/qml/delegates/TimelineRow.qml +++ b/resources/qml/delegates/TimelineRow.qml @@ -1,5 +1,5 @@ import QtQuick 2.6 -import QtQuick.Controls 2.1 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 @@ -48,8 +48,12 @@ RowLayout { id: replyButton flat: true Layout.preferredHeight: 16 - ToolTip.visible: hovered - ToolTip.text: qsTr("Reply") + + ToolTip { + visible: replyButton.hovered + text: qsTr("Reply") + palette: colors + } // disable background, because we don't want a border on hover background: Item { @@ -74,8 +78,12 @@ RowLayout { id: optionsButton flat: true Layout.preferredHeight: 16 - ToolTip.visible: hovered - ToolTip.text: qsTr("Options") + + ToolTip { + visible: optionsButton.hovered + text: qsTr("Options") + palette: colors + } // disable background, because we don't want a border on hover background: Item { @@ -98,6 +106,7 @@ RowLayout { Menu { y: optionsButton.height id: contextMenu + palette: colors MenuItem { text: qsTr("Read receipts") @@ -127,13 +136,16 @@ RowLayout { text: model.timestamp.toLocaleTimeString("HH:mm") color: inactiveColors.text - ToolTip.visible: ma.containsMouse - ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) - MouseArea{ id: ma anchors.fill: parent hoverEnabled: true } + + ToolTip { + visible: ma.containsMouse + text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) + palette: colors + } } } diff --git a/src/Utils.cpp b/src/Utils.cpp index d458dbcc..5a1447ac 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -323,19 +323,29 @@ utils::linkifyMessage(const QString &body) return doc; } -QByteArray escapeRawHtml(const QByteArray &data) { - QByteArray buffer; - const size_t length = data.size(); - buffer.reserve(length); - for(size_t pos = 0; pos != length; ++pos) { - switch(data.at(pos)) { - case '&': buffer.append("&"); break; - case '<': buffer.append("<"); break; - case '>': buffer.append(">"); break; - default: buffer.append(data.at(pos)); break; - } - } - return buffer; +QByteArray +escapeRawHtml(const QByteArray &data) +{ + QByteArray buffer; + const size_t length = data.size(); + buffer.reserve(length); + for (size_t pos = 0; pos != length; ++pos) { + switch (data.at(pos)) { + case '&': + buffer.append("&"); + break; + case '<': + buffer.append("<"); + break; + case '>': + buffer.append(">"); + break; + default: + buffer.append(data.at(pos)); + break; + } + } + return buffer; } QString diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 13025864..057f03de 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -3,13 +3,51 @@ #include #include #include +#include #include #include +#include "ChatPage.h" #include "Logging.h" #include "MxcImageProvider.h" +#include "UserSettingsPage.h" #include "dialogs/ImageOverlay.h" +void +TimelineViewManager::updateColorPalette() +{ + UserSettings settings; + if (settings.theme() == "light") { + QPalette lightActive(/*windowText*/ QColor("#333"), + /*button*/ QColor("#333"), + /*light*/ QColor(), + /*dark*/ QColor(220, 220, 220, 120), + /*mid*/ QColor(), + /*text*/ QColor("#333"), + /*bright_text*/ QColor(), + /*base*/ QColor("white"), + /*window*/ QColor("white")); + view->rootContext()->setContextProperty("currentActivePalette", lightActive); + view->rootContext()->setContextProperty("currentInactivePalette", lightActive); + } else if (settings.theme() == "dark") { + QPalette darkActive(/*windowText*/ QColor("#caccd1"), + /*button*/ QColor("#caccd1"), + /*light*/ QColor(), + /*dark*/ QColor(45, 49, 57, 120), + /*mid*/ QColor(), + /*text*/ QColor("#caccd1"), + /*bright_text*/ QColor(), + /*base*/ QColor("#202228"), + /*window*/ QColor("#202228")); + darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9")); + view->rootContext()->setContextProperty("currentActivePalette", darkActive); + view->rootContext()->setContextProperty("currentInactivePalette", darkActive); + } else { + view->rootContext()->setContextProperty("currentActivePalette", QPalette()); + view->rootContext()->setContextProperty("currentInactivePalette", nullptr); + } +} + TimelineViewManager::TimelineViewManager(QWidget *parent) : imgProvider(new MxcImageProvider()) { @@ -23,8 +61,14 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) container = QWidget::createWindowContainer(view, parent); container->setMinimumSize(200, 200); view->rootContext()->setContextProperty("timelineManager", this); + updateColorPalette(); view->engine()->addImageProvider("MxcImage", imgProvider); view->setSource(QUrl("qrc:///qml/TimelineView.qml")); + + connect(dynamic_cast(parent), + &ChatPage::themeChanged, + this, + &TimelineViewManager::updateColorPalette); } void diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index 6a6d3c6b..b14e78ff 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -71,6 +71,7 @@ public slots: void initWithMessages(const std::map &msgs); void setHistoryView(const QString &room_id); + void updateColorPalette(); void queueTextMessage(const QString &msg); void queueReplyMessage(const QString &reply, const RelatedInfo &related); From c37495fae29cd93b1b03e4e3689c604cc5312a18 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 20 Oct 2019 12:39:47 +0200 Subject: [PATCH 64/94] Use a basic implementation of a DelegateChooser for compat with older Qt The interface is taken from Qt/KDE, but the implementation is different, because the Qt implementation depends on some Qt internals. --- CMakeLists.txt | 3 + resources/qml/RowDelegateChooser.qml | 52 +++++++++ resources/qml/TimelineView.qml | 46 +------- resources/res.qrc | 1 + src/timeline2/DelegateChooser.cpp | 160 ++++++++++++++++++++++++++ src/timeline2/DelegateChooser.h | 78 +++++++++++++ src/timeline2/TimelineViewManager.cpp | 4 + 7 files changed, 299 insertions(+), 45 deletions(-) create mode 100644 resources/qml/RowDelegateChooser.qml create mode 100644 src/timeline2/DelegateChooser.cpp create mode 100644 src/timeline2/DelegateChooser.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f659d91c..f6249831 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -193,6 +193,7 @@ set(SRC_FILES # Timeline src/timeline2/TimelineViewManager.cpp src/timeline2/TimelineModel.cpp + src/timeline2/DelegateChooser.cpp #src/timeline/TimelineViewManager.cpp #src/timeline/TimelineItem.cpp #src/timeline/TimelineView.cpp @@ -338,6 +339,7 @@ qt5_wrap_cpp(MOC_HEADERS # Timeline src/timeline2/TimelineViewManager.h src/timeline2/TimelineModel.h + src/timeline2/DelegateChooser.h #src/timeline/TimelineItem.h #src/timeline/TimelineView.h #src/timeline/TimelineViewManager.h @@ -410,6 +412,7 @@ set(COMMON_LIBS Qt5::Concurrent Qt5::Multimedia Qt5::Qml + Qt5::QmlPrivate Qt5::QuickControls2 nlohmann_json::nlohmann_json) diff --git a/resources/qml/RowDelegateChooser.qml b/resources/qml/RowDelegateChooser.qml new file mode 100644 index 00000000..b7b6bdf4 --- /dev/null +++ b/resources/qml/RowDelegateChooser.qml @@ -0,0 +1,52 @@ +import QtQuick 2.6 +import Qt.labs.qmlmodels 1.0 +import com.github.nheko 1.0 + +import "./delegates" + +DelegateChooser { + role: "type" + width: chat.width + roleValue: model.type + + DelegateChoice { + roleValue: MtxEvent.TextMessage + TimelineRow { view: chat; TextMessage { id: kid } } + } + DelegateChoice { + roleValue: MtxEvent.NoticeMessage + TimelineRow { view: chat; NoticeMessage { id: kid } } + } + DelegateChoice { + roleValue: MtxEvent.EmoteMessage + TimelineRow { view: chat; TextMessage { id: kid } } + } + DelegateChoice { + roleValue: MtxEvent.ImageMessage + TimelineRow { view: chat; ImageMessage { id: kid } } + } + DelegateChoice { + roleValue: MtxEvent.Sticker + TimelineRow { view: chat; ImageMessage { id: kid } } + } + DelegateChoice { + roleValue: MtxEvent.FileMessage + TimelineRow { view: chat; FileMessage { id: kid } } + } + DelegateChoice { + roleValue: MtxEvent.VideoMessage + TimelineRow { view: chat; PlayableMediaMessage { id: kid } } + } + DelegateChoice { + roleValue: MtxEvent.AudioMessage + TimelineRow { view: chat; PlayableMediaMessage { id: kid } } + } + DelegateChoice { + roleValue: MtxEvent.Redacted + TimelineRow { view: chat; Redacted { id: kid } } + } + DelegateChoice { + //roleValue: MtxEvent.Redacted + TimelineRow { view: chat; Placeholder { id: kid } } + } +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index d1ada3ea..e09b9ed3 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -3,7 +3,6 @@ import QtQuick.Controls 2.1 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 -import Qt.labs.qmlmodels 1.0 import com.github.nheko 1.0 @@ -91,50 +90,7 @@ Rectangle { onMovementEnded: updatePosition() spacing: 4 - delegate: DelegateChooser { - role: "type" - DelegateChoice { - roleValue: MtxEvent.TextMessage - TimelineRow { view: chat; TextMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.NoticeMessage - TimelineRow { view: chat; NoticeMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.EmoteMessage - TimelineRow { view: chat; TextMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.ImageMessage - TimelineRow { view: chat; ImageMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.Sticker - TimelineRow { view: chat; ImageMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.FileMessage - TimelineRow { view: chat; FileMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.VideoMessage - TimelineRow { view: chat; PlayableMediaMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.AudioMessage - TimelineRow { view: chat; PlayableMediaMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.Redacted - TimelineRow { view: chat; Redacted { id: kid } } - } - DelegateChoice { - //roleValue: MtxEvent.Redacted - TimelineRow { view: chat; Placeholder { id: kid } } - } - } - + delegate: RowDelegateChooser {} section { property: "section" diff --git a/resources/res.qrc b/resources/res.qrc index 11a20e54..4816ffad 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -116,6 +116,7 @@ qml/TimelineView.qml + qml/RowDelegateChooser.qml qml/Avatar.qml qml/StatusIndicator.qml qml/EncryptionIndicator.qml diff --git a/src/timeline2/DelegateChooser.cpp b/src/timeline2/DelegateChooser.cpp new file mode 100644 index 00000000..ddde93e1 --- /dev/null +++ b/src/timeline2/DelegateChooser.cpp @@ -0,0 +1,160 @@ +#include "DelegateChooser.h" + +#include "Logging.h" + +// uses private API, which moved between versions +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) +#include +#else +#include +#endif + +QQmlComponent * +DelegateChoice::delegate() const +{ + return delegate_; +} + +void +DelegateChoice::setDelegate(QQmlComponent *delegate) +{ + if (delegate != delegate_) { + delegate_ = delegate; + emit delegateChanged(); + emit changed(); + } +} + +QVariant +DelegateChoice::roleValue() const +{ + return roleValue_; +} + +void +DelegateChoice::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + emit roleValueChanged(); + emit changed(); + } +} + +QVariant +DelegateChooser::roleValue() const +{ + return roleValue_; +} + +void +DelegateChooser::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + recalcChild(); + emit roleValueChanged(); + } +} + +QQmlListProperty +DelegateChooser::choices() +{ + return QQmlListProperty(this, + this, + &DelegateChooser::appendChoice, + &DelegateChooser::choiceCount, + &DelegateChooser::choice, + &DelegateChooser::clearChoices); +} + +QString +DelegateChooser::role() const +{ + return role_; +} + +void +DelegateChooser::setRole(const QString &role) +{ + if (role != role_) { + role_ = role; + emit roleChanged(); + } +} + +QQmlComponent * +DelegateChooser::delegate(QQmlAdaptorModel *adaptorModel, int row, int column) const +{ + auto value = adaptorModel->value(adaptorModel->indexAt(row, column), role_); + + for (const auto choice : choices_) { + auto choiceValue = choice->roleValue(); + if (!value.isValid() || choiceValue == value) { + nhlog::ui()->debug("Returned delegate for {}", role_.toStdString()); + return choice->delegate(); + } + } + + nhlog::ui()->debug("Returned null delegate"); + return nullptr; +} + +void +DelegateChooser::appendChoice(QQmlListProperty *p, DelegateChoice *c) +{ + DelegateChooser *dc = static_cast(p->object); + dc->choices_.append(c); + // dc->recalcChild(); +} + +int +DelegateChooser::choiceCount(QQmlListProperty *p) +{ + return static_cast(p->object)->choices_.count(); +} +DelegateChoice * +DelegateChooser::choice(QQmlListProperty *p, int index) +{ + return static_cast(p->object)->choices_.at(index); +} +void +DelegateChooser::clearChoices(QQmlListProperty *p) +{ + static_cast(p->object)->choices_.clear(); +} + +void +DelegateChooser::recalcChild() +{ + for (const auto choice : choices_) { + auto choiceValue = choice->roleValue(); + if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { + nhlog::ui()->debug("Returned delegate for {}", role_.toStdString()); + + if (child) { + // delete child; + child = nullptr; + } + + child = dynamic_cast( + choice->delegate()->create(QQmlEngine::contextForObject(this))); + child->setParentItem(this); + connect(this->child, &QQuickItem::heightChanged, this, [this]() { + this->setHeight(this->child->height()); + }); + this->setHeight(this->child->height()); + return; + } + } +} + +void +DelegateChooser::componentComplete() +{ + QQuickItem::componentComplete(); + recalcChild(); +} + diff --git a/src/timeline2/DelegateChooser.h b/src/timeline2/DelegateChooser.h new file mode 100644 index 00000000..d2a1cf59 --- /dev/null +++ b/src/timeline2/DelegateChooser.h @@ -0,0 +1,78 @@ +// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt versions +// see KDE/kquickitemviews +// see qtdeclarative/qqmldelagatecomponent + +#pragma once + +#include +#include +#include +#include +#include + +class QQmlAdaptorModel; + +class DelegateChoice : public QObject +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "delegate") + +public: + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + +signals: + void delegateChanged(); + void roleValueChanged(); + void changed(); + +private: + QVariant roleValue_; + QQmlComponent *delegate_ = nullptr; +}; + +class DelegateChooser : public QQuickItem +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "choices") + +public: + Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT) + Q_PROPERTY(QString role READ role WRITE setRole NOTIFY roleChanged) + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + + QQmlListProperty choices(); + + QString role() const; + void setRole(const QString &role); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + + QQmlComponent *delegate(QQmlAdaptorModel *adaptorModel, int row, int column = 0) const; + + void recalcChild(); + void componentComplete() override; + +signals: + void roleChanged(); + void roleValueChanged(); + +private: + QString role_; + QVariant roleValue_; + QList choices_; + QQuickItem *child; + + static void appendChoice(QQmlListProperty *, DelegateChoice *); + static int choiceCount(QQmlListProperty *); + static DelegateChoice *choice(QQmlListProperty *, int index); + static void clearChoices(QQmlListProperty *); +}; + diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 057f03de..a054bc78 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -8,6 +8,7 @@ #include #include "ChatPage.h" +#include "DelegateChooser.h" #include "Logging.h" #include "MxcImageProvider.h" #include "UserSettingsPage.h" @@ -57,6 +58,9 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) 0, "MtxEvent", "Can't instantiate enum!"); + qmlRegisterType("com.github.nheko", 1, 0, "DelegateChoice"); + qmlRegisterType("com.github.nheko", 1, 0, "DelegateChooser"); + view = new QQuickView(); container = QWidget::createWindowContainer(view, parent); container->setMinimumSize(200, 200); From c8f97216faa74b9d3f1f92af7acc509dbe6b5647 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 25 Oct 2019 11:46:25 +0200 Subject: [PATCH 65/94] Small fixes to delegate chooser implementation --- resources/qml/RowDelegateChooser.qml | 3 +-- resources/qml/TimelineView.qml | 10 ++++++- src/timeline2/DelegateChooser.cpp | 40 ---------------------------- src/timeline2/DelegateChooser.h | 7 ----- 4 files changed, 10 insertions(+), 50 deletions(-) diff --git a/resources/qml/RowDelegateChooser.qml b/resources/qml/RowDelegateChooser.qml index b7b6bdf4..bacd970a 100644 --- a/resources/qml/RowDelegateChooser.qml +++ b/resources/qml/RowDelegateChooser.qml @@ -5,7 +5,7 @@ import com.github.nheko 1.0 import "./delegates" DelegateChooser { - role: "type" + //role: "type" //< not supported in our custom implementation, have to use roleValue width: chat.width roleValue: model.type @@ -46,7 +46,6 @@ DelegateChooser { TimelineRow { view: chat; Redacted { id: kid } } } DelegateChoice { - //roleValue: MtxEvent.Redacted TimelineRow { view: chat; Placeholder { id: kid } } } } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index e09b9ed3..4e379567 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -90,7 +90,15 @@ Rectangle { onMovementEnded: updatePosition() spacing: 4 - delegate: RowDelegateChooser {} + delegate: RowDelegateChooser { + function isFullyVisible() { + return height > 1 && (y - chat.contentY - 1) + height < chat.height + } + function getIndex() { + return index; + } + + } section { property: "section" diff --git a/src/timeline2/DelegateChooser.cpp b/src/timeline2/DelegateChooser.cpp index ddde93e1..b86fc6cc 100644 --- a/src/timeline2/DelegateChooser.cpp +++ b/src/timeline2/DelegateChooser.cpp @@ -5,11 +5,6 @@ // uses private API, which moved between versions #include #include -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) -#include -#else -#include -#endif QQmlComponent * DelegateChoice::delegate() const @@ -70,44 +65,11 @@ DelegateChooser::choices() &DelegateChooser::clearChoices); } -QString -DelegateChooser::role() const -{ - return role_; -} - -void -DelegateChooser::setRole(const QString &role) -{ - if (role != role_) { - role_ = role; - emit roleChanged(); - } -} - -QQmlComponent * -DelegateChooser::delegate(QQmlAdaptorModel *adaptorModel, int row, int column) const -{ - auto value = adaptorModel->value(adaptorModel->indexAt(row, column), role_); - - for (const auto choice : choices_) { - auto choiceValue = choice->roleValue(); - if (!value.isValid() || choiceValue == value) { - nhlog::ui()->debug("Returned delegate for {}", role_.toStdString()); - return choice->delegate(); - } - } - - nhlog::ui()->debug("Returned null delegate"); - return nullptr; -} - void DelegateChooser::appendChoice(QQmlListProperty *p, DelegateChoice *c) { DelegateChooser *dc = static_cast(p->object); dc->choices_.append(c); - // dc->recalcChild(); } int @@ -132,8 +94,6 @@ DelegateChooser::recalcChild() for (const auto choice : choices_) { auto choiceValue = choice->roleValue(); if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { - nhlog::ui()->debug("Returned delegate for {}", role_.toStdString()); - if (child) { // delete child; child = nullptr; diff --git a/src/timeline2/DelegateChooser.h b/src/timeline2/DelegateChooser.h index d2a1cf59..7350e0d3 100644 --- a/src/timeline2/DelegateChooser.h +++ b/src/timeline2/DelegateChooser.h @@ -44,19 +44,13 @@ class DelegateChooser : public QQuickItem public: Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT) - Q_PROPERTY(QString role READ role WRITE setRole NOTIFY roleChanged) Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) QQmlListProperty choices(); - QString role() const; - void setRole(const QString &role); - QVariant roleValue() const; void setRoleValue(const QVariant &value); - QQmlComponent *delegate(QQmlAdaptorModel *adaptorModel, int row, int column = 0) const; - void recalcChild(); void componentComplete() override; @@ -65,7 +59,6 @@ signals: void roleValueChanged(); private: - QString role_; QVariant roleValue_; QList choices_; QQuickItem *child; From 3d6f502bcc4bae477eb3f8d51aa7b90a6c9e9f46 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 25 Oct 2019 13:20:05 +0200 Subject: [PATCH 66/94] Incubate delegates asynchronously --- resources/qml/TimelineView.qml | 2 +- src/timeline2/DelegateChooser.cpp | 35 ++++++++++++++++++++++++------- src/timeline2/DelegateChooser.h | 15 ++++++++++++- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 4e379567..046f7800 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -29,7 +29,7 @@ Rectangle { ListView { id: chat - cacheBuffer: parent.height + cacheBuffer: 2000 visible: timelineManager.timeline != null anchors.fill: parent diff --git a/src/timeline2/DelegateChooser.cpp b/src/timeline2/DelegateChooser.cpp index b86fc6cc..e558da61 100644 --- a/src/timeline2/DelegateChooser.cpp +++ b/src/timeline2/DelegateChooser.cpp @@ -95,17 +95,11 @@ DelegateChooser::recalcChild() auto choiceValue = choice->roleValue(); if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { if (child) { - // delete child; + child->setParentItem(nullptr); child = nullptr; } - child = dynamic_cast( - choice->delegate()->create(QQmlEngine::contextForObject(this))); - child->setParentItem(this); - connect(this->child, &QQuickItem::heightChanged, this, [this]() { - this->setHeight(this->child->height()); - }); - this->setHeight(this->child->height()); + choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); return; } } @@ -118,3 +112,28 @@ DelegateChooser::componentComplete() recalcChild(); } +void +DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) +{ + if (status == QQmlIncubator::Ready) { + chooser.child = dynamic_cast(object()); + if (chooser.child == nullptr) { + nhlog::ui()->error("Delegate has to be derived of Item!"); + delete chooser.child; + return; + } + + chooser.child->setParentItem(&chooser); + connect(chooser.child, &QQuickItem::heightChanged, &chooser, [this]() { + chooser.setHeight(chooser.child->height()); + }); + chooser.setHeight(chooser.child->height()); + QQmlEngine::setObjectOwnership(chooser.child, + QQmlEngine::ObjectOwnership::JavaScriptOwnership); + + } else if (status == QQmlIncubator::Error) { + for (const auto &e : errors()) + nhlog::ui()->error("Error instantiating delegate: {}", + e.toString().toStdString()); + } +} diff --git a/src/timeline2/DelegateChooser.h b/src/timeline2/DelegateChooser.h index 7350e0d3..a20a1489 100644 --- a/src/timeline2/DelegateChooser.h +++ b/src/timeline2/DelegateChooser.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include @@ -59,9 +60,21 @@ signals: void roleValueChanged(); private: + struct DelegateIncubator : public QQmlIncubator + { + DelegateIncubator(DelegateChooser &parent) + : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) + , chooser(parent) + {} + void statusChanged(QQmlIncubator::Status status) override; + + DelegateChooser &chooser; + }; + QVariant roleValue_; QList choices_; - QQuickItem *child; + QQuickItem *child = nullptr; + DelegateIncubator incubator{*this}; static void appendChoice(QQmlListProperty *, DelegateChoice *); static int choiceCount(QQmlListProperty *); From 0d8bf6c67693cc1e41bd9fab7ba7924088506b84 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 25 Oct 2019 14:20:43 +0200 Subject: [PATCH 67/94] lint --- src/timeline2/DelegateChooser.cpp | 2 +- src/timeline2/DelegateChooser.h | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/timeline2/DelegateChooser.cpp b/src/timeline2/DelegateChooser.cpp index e558da61..6aeea69b 100644 --- a/src/timeline2/DelegateChooser.cpp +++ b/src/timeline2/DelegateChooser.cpp @@ -9,7 +9,7 @@ QQmlComponent * DelegateChoice::delegate() const { - return delegate_; + return delegate_; } void diff --git a/src/timeline2/DelegateChooser.h b/src/timeline2/DelegateChooser.h index a20a1489..68ebeb04 100644 --- a/src/timeline2/DelegateChooser.h +++ b/src/timeline2/DelegateChooser.h @@ -1,6 +1,5 @@ -// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt versions -// see KDE/kquickitemviews -// see qtdeclarative/qqmldelagatecomponent +// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt +// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent #pragma once @@ -40,8 +39,8 @@ private: class DelegateChooser : public QQuickItem { - Q_OBJECT - Q_CLASSINFO("DefaultProperty", "choices") + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "choices") public: Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT) @@ -81,4 +80,3 @@ private: static DelegateChoice *choice(QQmlListProperty *, int index); static void clearChoices(QQmlListProperty *); }; - From e52ff609ed779c2af648e16f73fef5f27be188e0 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 25 Oct 2019 16:54:21 +0200 Subject: [PATCH 68/94] Remove unused Qt Module --- CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f6249831..368b754a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -412,7 +412,6 @@ set(COMMON_LIBS Qt5::Concurrent Qt5::Multimedia Qt5::Qml - Qt5::QmlPrivate Qt5::QuickControls2 nlohmann_json::nlohmann_json) From 2055c75f8ba710e8950a55aa3c41a9cec9f26ad7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 27 Oct 2019 22:01:40 +0100 Subject: [PATCH 69/94] Organize qml files a bit --- CMakeLists.txt | 1 + cmake/Translations.cmake | 7 +++++- resources/qml/Avatar.qml | 1 + resources/qml/{delegates => }/TimelineRow.qml | 19 +++++--------- resources/qml/TimelineView.qml | 3 +-- resources/qml/delegates/FileMessage.qml | 2 +- resources/qml/delegates/ImageMessage.qml | 2 +- .../MessageDelegate.qml} | 25 +++++++++---------- resources/qml/delegates/NoticeMessage.qml | 2 +- resources/qml/delegates/Placeholder.qml | 2 +- .../qml/delegates/PlayableMediaMessage.qml | 2 +- resources/qml/delegates/TextMessage.qml | 2 +- resources/res.qrc | 4 +-- 13 files changed, 35 insertions(+), 37 deletions(-) rename resources/qml/{delegates => }/TimelineRow.qml (90%) rename resources/qml/{RowDelegateChooser.qml => delegates/MessageDelegate.qml} (54%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 368b754a..ae9a5e46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,7 @@ include(LMDB) # Discover Qt dependencies. # find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 REQUIRED) +find_package(Qt5QuickCompiler) find_package(Qt5DBus) if (APPLE) diff --git a/cmake/Translations.cmake b/cmake/Translations.cmake index 8ca91883..bdd8ecbc 100644 --- a/cmake/Translations.cmake +++ b/cmake/Translations.cmake @@ -21,4 +21,9 @@ if(NOT EXISTS ${_qrc}) endif() qt5_add_resources(LANG_QRC ${_qrc}) -qt5_add_resources(QRC resources/res.qrc) +#qt5_add_resources(QRC resources/res.qrc) +if(Qt5QuickCompiler_FOUND) + qtquick_compiler_add_resources(QRC resources/res.qrc) +else() + qt5_add_resources(QRC resources/res.qrc) +endif() diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 9d7b54fe..131e6b46 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -30,6 +30,7 @@ Rectangle { id: img anchors.fill: parent asynchronous: true + fillMode: Image.PreserveAspectCrop layer.enabled: true layer.effect: OpacityMask { diff --git a/resources/qml/delegates/TimelineRow.qml b/resources/qml/TimelineRow.qml similarity index 90% rename from resources/qml/delegates/TimelineRow.qml rename to resources/qml/TimelineRow.qml index 3019deb1..fdc2ec95 100644 --- a/resources/qml/delegates/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -6,29 +6,22 @@ import QtQuick.Window 2.2 import com.github.nheko 1.0 -import ".." +import "./delegates" RowLayout { - property var view: undefined - default property alias data: contentItem.data + property var view: chat - height: kid.height // TODO: fix this, we shouldn't need to give the child of contentItem this id! anchors.leftMargin: avatarSize + 4 + anchors.rightMargin: scrollbar.width anchors.left: parent.left anchors.right: parent.right - anchors.rightMargin: scrollbar.width - function isFullyVisible() { - return (y - view.contentY - 1) + height < view.height - } - function getIndex() { - return index; - } + height: contentItem.height - Item { - id: contentItem + MessageDelegate { Layout.fillWidth: true Layout.alignment: Qt.AlignTop + id: contentItem } StatusIndicator { diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 046f7800..e5c1bda6 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -90,14 +90,13 @@ Rectangle { onMovementEnded: updatePosition() spacing: 4 - delegate: RowDelegateChooser { + delegate: TimelineRow { function isFullyVisible() { return height > 1 && (y - chat.contentY - 1) + height < chat.height } function getIndex() { return index; } - } section { diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index ad2c695d..f4cf3f15 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -5,7 +5,7 @@ Rectangle { radius: 10 color: colors.dark height: row.height + 24 - width: parent.width + width: parent ? parent.width : undefined RowLayout { id: row diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index f1e95e3d..802ef721 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -3,7 +3,7 @@ import QtQuick 2.6 import com.github.nheko 1.0 Item { - width: Math.min(parent.width, model.width) + width: Math.min(parent ? parent.width : undefined, model.width) height: width * model.proportionalHeight Image { diff --git a/resources/qml/RowDelegateChooser.qml b/resources/qml/delegates/MessageDelegate.qml similarity index 54% rename from resources/qml/RowDelegateChooser.qml rename to resources/qml/delegates/MessageDelegate.qml index bacd970a..3d342a02 100644 --- a/resources/qml/RowDelegateChooser.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -2,50 +2,49 @@ import QtQuick 2.6 import Qt.labs.qmlmodels 1.0 import com.github.nheko 1.0 -import "./delegates" - DelegateChooser { //role: "type" //< not supported in our custom implementation, have to use roleValue - width: chat.width roleValue: model.type + width: parent.width + DelegateChoice { roleValue: MtxEvent.TextMessage - TimelineRow { view: chat; TextMessage { id: kid } } + TextMessage {} } DelegateChoice { roleValue: MtxEvent.NoticeMessage - TimelineRow { view: chat; NoticeMessage { id: kid } } + NoticeMessage {} } DelegateChoice { roleValue: MtxEvent.EmoteMessage - TimelineRow { view: chat; TextMessage { id: kid } } + TextMessage {} } DelegateChoice { roleValue: MtxEvent.ImageMessage - TimelineRow { view: chat; ImageMessage { id: kid } } + ImageMessage {} } DelegateChoice { roleValue: MtxEvent.Sticker - TimelineRow { view: chat; ImageMessage { id: kid } } + ImageMessage {} } DelegateChoice { roleValue: MtxEvent.FileMessage - TimelineRow { view: chat; FileMessage { id: kid } } + FileMessage {} } DelegateChoice { roleValue: MtxEvent.VideoMessage - TimelineRow { view: chat; PlayableMediaMessage { id: kid } } + PlayableMediaMessage {} } DelegateChoice { roleValue: MtxEvent.AudioMessage - TimelineRow { view: chat; PlayableMediaMessage { id: kid } } + PlayableMediaMessage {} } DelegateChoice { roleValue: MtxEvent.Redacted - TimelineRow { view: chat; Redacted { id: kid } } + Redacted {} } DelegateChoice { - TimelineRow { view: chat; Placeholder { id: kid } } + Placeholder {} } } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index b916d65a..59e051be 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -5,7 +5,7 @@ TextEdit { textFormat: TextEdit.RichText readOnly: true wrapMode: Text.Wrap - width: parent.width + width: parent ? parent.width : undefined selectByMouse: true font.italic: true color: inactiveColors.text diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml index 462af2db..171bf18d 100644 --- a/resources/qml/delegates/Placeholder.qml +++ b/resources/qml/delegates/Placeholder.qml @@ -5,6 +5,6 @@ Label { text: qsTr("unimplemented event: ") + model.type textFormat: Text.PlainText wrapMode: Text.Wrap - width: parent.width + width: parent ? parent.width : undefined color: inactiveColors.text } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 3a518617..68b09f7b 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -10,7 +10,7 @@ Rectangle { radius: 10 color: colors.dark height: content.height + 24 - width: parent.width + width: parent ? parent.width : undefined Column { id: content diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 3a3492ed..713be868 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -5,7 +5,7 @@ TextEdit { textFormat: TextEdit.RichText readOnly: true wrapMode: Text.Wrap - width: parent.width + width: parent ? parent.width : undefined selectByMouse: true color: colors.text } diff --git a/resources/res.qrc b/resources/res.qrc index 4816ffad..86b1364c 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -116,11 +116,11 @@ qml/TimelineView.qml - qml/RowDelegateChooser.qml qml/Avatar.qml qml/StatusIndicator.qml qml/EncryptionIndicator.qml - qml/delegates/TimelineRow.qml + qml/TimelineRow.qml + qml/delegates/MessageDelegate.qml qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml From b1f1cb2b560aa56d485ba1e326bf111326c7aa74 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 27 Oct 2019 22:49:49 +0100 Subject: [PATCH 70/94] Redirect qt logger --- src/Logging.cpp | 39 +++++++++++++++++++++++++++++++++++++++ src/Logging.h | 3 +++ 2 files changed, 42 insertions(+) diff --git a/src/Logging.cpp b/src/Logging.cpp index 32287582..b5952aeb 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -5,14 +5,43 @@ #include "spdlog/sinks/stdout_color_sinks.h" #include +#include +#include + namespace { std::shared_ptr db_logger = nullptr; std::shared_ptr net_logger = nullptr; std::shared_ptr crypto_logger = nullptr; std::shared_ptr ui_logger = nullptr; +std::shared_ptr qml_logger = nullptr; constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6; constexpr auto MAX_LOG_FILES = 3; + +void +qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + std::string localMsg = msg.toStdString(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + switch (type) { + case QtDebugMsg: + nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtInfoMsg: + nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtWarningMsg: + nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtCriticalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtFatalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + } +} } namespace nhlog { @@ -35,12 +64,15 @@ init(const std::string &file_path) db_logger = std::make_shared("db", std::begin(sinks), std::end(sinks)); crypto_logger = std::make_shared("crypto", std::begin(sinks), std::end(sinks)); + qml_logger = std::make_shared("qml", std::begin(sinks), std::end(sinks)); if (nheko::enable_debug_log) { db_logger->set_level(spdlog::level::trace); ui_logger->set_level(spdlog::level::trace); crypto_logger->set_level(spdlog::level::trace); } + + qInstallMessageHandler(qmlMessageHandler); } std::shared_ptr @@ -66,4 +98,11 @@ crypto() { return crypto_logger; } + +std::shared_ptr +qml() +{ + return qml_logger; } +} + diff --git a/src/Logging.h b/src/Logging.h index e54f3c3f..f572afae 100644 --- a/src/Logging.h +++ b/src/Logging.h @@ -19,5 +19,8 @@ db(); std::shared_ptr crypto(); +std::shared_ptr +qml(); + extern bool enable_debug_log_from_commandline; } From 15badebc7735d554a39cf0eb50e400ee95c1e0c8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 28 Oct 2019 20:39:02 +0100 Subject: [PATCH 71/94] Show own messages in RoomList --- src/timeline2/TimelineModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index b37ade54..45126b36 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -525,7 +525,7 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); endInsertRows(); - for (auto id = ids.rbegin(); id != ids.rend(); id++) { + for (auto id = eventOrder.rbegin(); id != eventOrder.rend(); id++) { auto event = events.value(*id); if (auto e = boost::get>( &event)) { From 3c9ddc2afbf0495722a9a3d68e7c1e3513145427 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 30 Oct 2019 00:53:56 +0100 Subject: [PATCH 72/94] break height binding loop --- resources/qml/TimelineRow.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index fdc2ec95..66d44622 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -16,7 +16,7 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right - height: contentItem.height + implicitHeight: contentItem.childrenRect.height MessageDelegate { Layout.fillWidth: true From 6b6085b270bcdffe56e19de1cd1171a73fe5fba1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 31 Oct 2019 14:09:51 +0100 Subject: [PATCH 73/94] Actually fix updating roomlist on new messages --- src/Logging.cpp | 1 - src/timeline2/TimelineModel.cpp | 57 +++++++++++++++++---------------- src/timeline2/TimelineModel.h | 3 ++ 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/Logging.cpp b/src/Logging.cpp index b5952aeb..126b3781 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -105,4 +105,3 @@ qml() return qml_logger; } } - diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 45126b36..2428ddb6 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -348,6 +348,9 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj events.remove(txn_id); events.insert(event_id, ev); + // mark our messages as read + readEvent(event_id.toStdString()); + // ask to be notified for read receipts cache::client()->addPendingReceipt(room_id_, event_id); @@ -525,25 +528,20 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); endInsertRows(); - for (auto id = eventOrder.rbegin(); id != eventOrder.rend(); id++) { - auto event = events.value(*id); - if (auto e = boost::get>( - &event)) { - event = decryptEvent(*e).event; - } + updateLastMessage(); +} - auto type = boost::apply_visitor( - [](const auto &e) -> mtx::events::EventType { return e.type; }, event); - if (type == mtx::events::EventType::RoomMessage || - type == mtx::events::EventType::Sticker) { - auto description = utils::getMessageDescription( - event, - QString::fromStdString(http::client()->user_id().to_string()), - room_id_); - emit manager_->updateRoomsLastMessage(room_id_, description); - break; - } +void +TimelineModel::updateLastMessage() +{ + auto event = events.value(eventOrder.back()); + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; } + + auto description = utils::getMessageDescription( + event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); + emit manager_->updateRoomsLastMessage(room_id_, description); } std::vector @@ -634,20 +632,23 @@ TimelineModel::setCurrentIndex(int index) currentId = indexToId(index); emit currentIndexChanged(index); - if (oldIndex < index) { - http::client()->read_event(room_id_.toStdString(), - currentId.toStdString(), - [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read_event ({}, {})", - room_id_.toStdString(), - currentId.toStdString()); - } - }); + if (oldIndex < index && !pending.contains(currentId)) { + readEvent(currentId.toStdString()); } } +void +TimelineModel::readEvent(const std::string &id) +{ + http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to read_event ({}, {})", + room_id_.toStdString(), + currentId.toStdString()); + } + }); +} + void TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) { diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index b7ff546b..6a1f3438 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -192,6 +192,8 @@ private: 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; @@ -229,6 +231,7 @@ TimelineModel::sendMessage(const T &msg) 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)); From 2c37beba8dc5b32e843010ba117abb84951c820b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 2 Nov 2019 18:17:06 +0100 Subject: [PATCH 74/94] Fix translation of roomlist message preview This also makes long messages unreadable, because we don't shorten long usernames anymore. We may eventually want to do that again, but it is hard with translations and we probably want to shorten the displayname more, as before this change the message was only ever as long as the timestamp, which is usually just 5 characters... --- src/RoomInfoListItem.cpp | 30 ++---------- src/Utils.cpp | 5 -- src/Utils.h | 99 +++++++++++++++++++++++++--------------- 3 files changed, 67 insertions(+), 67 deletions(-) diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index f135451c..8bebb0f5 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -118,7 +118,7 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare // so we can't use them for sorting. if (roomType_ == RoomType::Invited) lastMsgInfo_ = { - emptyEventId, "-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)}; + emptyEventId, "-", "-", "-", QDateTime::currentDateTime().addYears(10)}; } void @@ -210,33 +210,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) p.setFont(QFont{}); p.setPen(subtitlePen); - // The limit is the space between the end of the avatar and the start of the - // timestamp. - int usernameLimit = - std::max(0, width() - 3 * wm.padding - msgStampWidth - wm.iconSize - 20); - auto userName = - metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit); - - p.setFont(QFont{}); - p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), userName); - -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - int nameWidth = QFontMetrics(QFont{}).width(userName); -#else - int nameWidth = QFontMetrics(QFont{}).horizontalAdvance(userName); -#endif - p.setFont(QFont{}); - - // The limit is the space between the end of the username and the start of - // the timestamp. - int descriptionLimit = - std::max(0, - width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize - - nameWidth - 5); + int descriptionLimit = std::max( + 0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize); auto description = metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit); - p.drawText(QPoint(2 * wm.padding + wm.iconSize + nameWidth, bottom_y), - description); + p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description); // We show the last message timestamp. p.save(); diff --git a/src/Utils.cpp b/src/Utils.cpp index 5a1447ac..e27bc995 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -146,11 +146,6 @@ utils::getMessageDescription(const TimelineEvent &event, const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); DescInfo info; - if (sender == localUser) - info.username = QCoreApplication::translate("utils", "You"); - else - info.username = username; - info.userid = sender; info.body = QString(" %1").arg(messageDescription()); info.timestamp = utils::descriptiveTime(ts); diff --git a/src/Utils.h b/src/Utils.h index 225754be..8cb891cc 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -94,38 +94,72 @@ messageDescription(const QString &username = "", using Video = mtx::events::RoomEvent; using Encrypted = mtx::events::EncryptedEvent; - // Sometimes the verb form of sent changes in some languages depending on the actor. - auto remoteSent = QCoreApplication::translate( - "message-description: ", "sent", "For when you are the sender"); - auto localSent = QCoreApplication::translate( - "message-description:", "sent", "For when someone else is the sender"); - QString sentVerb = isLocal ? localSent : remoteSent; if (std::is_same::value || std::is_same::value) { - return QCoreApplication::translate("message-description sent:", "%1 an audio clip") - .arg(sentVerb); + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an audio clip"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an audio clip") + .arg(username); } else if (std::is_same::value || std::is_same::value) { - return QCoreApplication::translate("message-description sent:", "%1 an image") - .arg(sentVerb); + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an image"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an image") + .arg(username); } else if (std::is_same::value || std::is_same::value) { - return QCoreApplication::translate("message-description sent:", "%1 a file") - .arg(sentVerb); + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a file"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a file") + .arg(username); } else if (std::is_same::value || std::is_same::value) { - return QCoreApplication::translate("message-description sent:", "%1 a video clip") - .arg(sentVerb); + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a video"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a video") + .arg(username); } else if (std::is_same::value || std::is_same::value) { - return QCoreApplication::translate("message-description sent:", "%1 a sticker") - .arg(sentVerb); + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a sticker"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a sticker") + .arg(username); } else if (std::is_same::value) { - return QCoreApplication::translate("message-description sent:", "%1 a notification") - .arg(sentVerb); + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a notification"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a notification") + .arg(username); } else if (std::is_same::value) { - return QString(": %1").arg(body); + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You: %1") + .arg(body); + else + return QCoreApplication::translate("message-description sent:", "%1: %2") + .arg(username) + .arg(body); } else if (std::is_same::value) { return QString("* %1 %2").arg(username).arg(body); } else if (std::is_same::value) { - return QCoreApplication::translate("message-description sent:", - "%1 an encrypted message") - .arg(sentVerb); + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an encrypted message"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an encrypted message") + .arg(username); } else { return QCoreApplication::translate("utils", "Unknown Message Type"); } @@ -144,20 +178,13 @@ createDescriptionInfo(const Event &event, const QString &localUser, const QStrin const auto username = Cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); - bool isText = std::is_same::value; - bool isEmote = std::is_same::value; - - return DescInfo{ - QString::fromStdString(msg.event_id), - isEmote ? "" - : (sender == localUser ? QCoreApplication::translate("utils", "You") : username), - sender, - (isText || isEmote) - ? messageDescription( - username, QString::fromStdString(msg.content.body).trimmed(), sender == localUser) - : QString(" %1").arg(messageDescription()), - utils::descriptiveTime(ts), - 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. From 2279484697c37dddae64474fb356a77f09d68c75 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 2 Nov 2019 20:06:22 +0100 Subject: [PATCH 75/94] Remove unused import --- resources/qml/delegates/MessageDelegate.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 3d342a02..3d892b76 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -1,5 +1,4 @@ import QtQuick 2.6 -import Qt.labs.qmlmodels 1.0 import com.github.nheko 1.0 DelegateChooser { From bde71a6cbcce17005c464d3cb77a8d8e5d3a8566 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 2 Nov 2019 21:14:22 +0100 Subject: [PATCH 76/94] fixup bad room list translation commit --- src/Cache.h | 1 - src/Utils.h | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/Cache.h b/src/Cache.h index 0da49793..f5e1cfa0 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -91,7 +91,6 @@ from_json(const json &j, ReadReceiptKey &key) struct DescInfo { QString event_id; - QString username; QString userid; QString body; QString timestamp; diff --git a/src/Utils.h b/src/Utils.h index 8cb891cc..007126c3 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -169,9 +169,6 @@ template DescInfo createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id) { - using Text = mtx::events::RoomEvent; - using Emote = mtx::events::RoomEvent; - const auto msg = boost::get(event); const auto sender = QString::fromStdString(msg.sender); From 4f7a45a0a6b28255fb4c1ccbb53fcf8a6958843a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 3 Nov 2019 01:17:40 +0100 Subject: [PATCH 77/94] Improve avatar look and layouting Thanks to red_sky for the feedback! --- resources/qml/Avatar.qml | 5 +++++ resources/qml/TimelineRow.qml | 1 - resources/qml/TimelineView.qml | 7 +++++-- resources/qml/delegates/MessageDelegate.qml | 2 -- src/MxcImageProvider.cpp | 6 ++---- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 131e6b46..a53f057b 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -31,6 +31,11 @@ Rectangle { anchors.fill: parent asynchronous: true fillMode: Image.PreserveAspectCrop + mipmap: true + smooth: false + + sourceSize.width: avatar.width + sourceSize.height: avatar.height layer.enabled: true layer.effect: OpacityMask { diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 66d44622..b9fa6f40 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -12,7 +12,6 @@ RowLayout { property var view: chat anchors.leftMargin: avatarSize + 4 - anchors.rightMargin: scrollbar.width anchors.left: parent.left anchors.right: parent.right diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index e5c1bda6..8f64637e 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -14,7 +14,7 @@ Rectangle { property var colors: currentActivePalette property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled } property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive - property int avatarSize: 32 + property int avatarSize: 40 color: colors.window @@ -34,6 +34,9 @@ Rectangle { visible: timelineManager.timeline != null anchors.fill: parent + anchors.leftMargin: 4 + anchors.rightMargin: scrollbar.width + model: timelineManager.timeline onModelChanged: { @@ -54,7 +57,7 @@ Rectangle { ScrollBar.vertical: ScrollBar { id: scrollbar anchors.top: parent.top - anchors.right: parent.right + anchors.left: parent.right anchors.bottom: parent.bottom onPressedChanged: if (!pressed) chat.updatePosition() } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 3d892b76..49209f68 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -5,8 +5,6 @@ DelegateChooser { //role: "type" //< not supported in our custom implementation, have to use roleValue roleValue: model.type - width: parent.width - DelegateChoice { roleValue: MtxEvent.TextMessage TextMessage {} diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index 86dbcabc..556b019b 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -6,7 +6,7 @@ void MxcImageResponse::run() { if (m_requestedSize.isValid()) { - QString fileName = QString("%1_%2x%3") + QString fileName = QString("%1_%2x%3_crop") .arg(m_id) .arg(m_requestedSize.width()) .arg(m_requestedSize.height()); @@ -23,7 +23,7 @@ MxcImageResponse::run() opts.mxc_url = "mxc://" + m_id.toStdString(); opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1; opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1; - opts.method = "scale"; + opts.method = "crop"; http::client()->get_thumbnail( opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) { if (err) { @@ -38,8 +38,6 @@ MxcImageResponse::run() auto data = QByteArray(res.data(), res.size()); cache::client()->saveImage(fileName, data); m_image.loadFromData(data); - m_image = m_image.scaled( - m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); m_image.setText("mxc url", "mxc://" + m_id); emit finished(); From 993926e189e213e9bb809c458cf5599d3aea055d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 3 Nov 2019 02:09:36 +0100 Subject: [PATCH 78/94] Make user clickable and improve button cursor look --- resources/qml/ImageButton.qml | 34 +++++++++++++++++++++++++ resources/qml/TimelineRow.qml | 45 +++++---------------------------- resources/qml/TimelineView.qml | 12 +++++++++ resources/res.qrc | 1 + src/timeline2/TimelineModel.cpp | 7 +++++ src/timeline2/TimelineModel.h | 1 + 6 files changed, 61 insertions(+), 39 deletions(-) create mode 100644 resources/qml/ImageButton.qml diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml new file mode 100644 index 00000000..dda9865b --- /dev/null +++ b/resources/qml/ImageButton.qml @@ -0,0 +1,34 @@ +import QtQuick 2.3 +import QtQuick.Controls 2.3 +import QtGraphicalEffects 1.0 + +Button { + property alias image: buttonImg.source + + id: button + + flat: true + + // disable background, because we don't want a border on hover + background: Item { + } + + Image { + id: buttonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + } + ColorOverlay { + anchors.fill: buttonImg + source: buttonImg + color: button.hovered ? colors.highlight : colors.buttonText + } + + MouseArea + { + id: mouseArea + anchors.fill: parent + onPressed: mouse.accepted = false + cursorShape: Qt.PointingHandCursor + } +} diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index b9fa6f40..c5c3fde0 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -1,7 +1,6 @@ import QtQuick 2.6 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 import com.github.nheko 1.0 @@ -35,64 +34,32 @@ RowLayout { Layout.preferredHeight: 16 } - Button { + ImageButton { Layout.alignment: Qt.AlignRight | Qt.AlignTop - id: replyButton - flat: true Layout.preferredHeight: 16 + id: replyButton + image: "qrc:/icons/icons/ui/mail-reply.png" ToolTip { visible: replyButton.hovered text: qsTr("Reply") palette: colors } - // disable background, because we don't want a border on hover - background: Item { - } - - Image { - id: replyButtonImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: "qrc:/icons/icons/ui/mail-reply.png" - } - ColorOverlay { - anchors.fill: replyButtonImg - source: replyButtonImg - color: replyButton.hovered ? colors.highlight : colors.buttonText - } - onClicked: view.model.replyAction(model.id) } - Button { + ImageButton { Layout.alignment: Qt.AlignRight | Qt.AlignTop - id: optionsButton - flat: true Layout.preferredHeight: 16 + id: optionsButton + image: "qrc:/icons/icons/ui/vertical-ellipsis.png" ToolTip { visible: optionsButton.hovered text: qsTr("Options") palette: colors } - // disable background, because we don't want a border on hover - background: Item { - } - - Image { - id: optionsButtonImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: "qrc:/icons/icons/ui/vertical-ellipsis.png" - } - ColorOverlay { - anchors.fill: optionsButtonImg - source: optionsButtonImg - color: optionsButton.hovered ? colors.highlight : colors.buttonText - } - onClicked: contextMenu.open() Menu { diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 8f64637e..c2f6f9b9 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -135,6 +135,12 @@ Rectangle { height: avatarSize url: chat.model.avatarUrl(section.split(" ")[0]).replace("mxc://", "image://MxcImage/") displayName: chat.model.displayName(section.split(" ")[0]) + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(section.split(" ")[0]) + cursorShape: Qt.PointingHandCursor + } } Text { @@ -142,6 +148,12 @@ Rectangle { text: chat.model.escapeEmoji(chat.model.displayName(section.split(" ")[0])) color: chat.model.userColor(section.split(" ")[0], colors.window) textFormat: Text.RichText + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(section.split(" ")[0]) + cursorShape: Qt.PointingHandCursor + } } } } diff --git a/resources/res.qrc b/resources/res.qrc index 86b1364c..264ed82d 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -117,6 +117,7 @@ qml/TimelineView.qml qml/Avatar.qml + qml/ImageButton.qml qml/StatusIndicator.qml qml/EncryptionIndicator.qml qml/TimelineRow.qml diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 2428ddb6..fa87ec26 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -713,6 +713,13 @@ TimelineModel::viewRawMessage(QString id) const Q_UNUSED(dialog); } +void + +TimelineModel::openUserProfile(QString userid) const +{ + MainWindow::instance()->openUserProfile(userid, room_id_); +} + DecryptionResult TimelineModel::decryptEvent(const mtx::events::EncryptedEvent &e) const { diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 6a1f3438..1ed6e72c 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -152,6 +152,7 @@ public: 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); From 88dc72df4f7cd6cabdb48866e6030f5e506eb24f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 3 Nov 2019 03:28:16 +0100 Subject: [PATCH 79/94] Enable link handling --- resources/qml/MatrixText.qml | 33 +++++++++++++++++++++++ resources/qml/delegates/NoticeMessage.qml | 8 ++---- resources/qml/delegates/Placeholder.qml | 7 ++--- resources/qml/delegates/TextMessage.qml | 9 ++----- resources/res.qrc | 1 + src/timeline2/TimelineModel.cpp | 1 + 6 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 resources/qml/MatrixText.qml diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml new file mode 100644 index 00000000..5d20095c --- /dev/null +++ b/resources/qml/MatrixText.qml @@ -0,0 +1,33 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.3 + +TextEdit { + textFormat: TextEdit.RichText + readOnly: true + wrapMode: Text.Wrap + selectByMouse: true + color: colors.text + + onLinkActivated: { + if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]) + if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) + if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { + var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link) + timelineManager.setHistoryView(match[1]) + chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain) + } + else Qt.openUrlExternally(link) + } + MouseArea + { + anchors.fill: parent + onPressed: mouse.accepted = false + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + + ToolTip { + visible: parent.hoveredLink + text: parent.hoveredLink + palette: colors + } +} diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index 59e051be..a392eb5b 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -1,12 +1,8 @@ -import QtQuick 2.5 +import ".." -TextEdit { +MatrixText { text: model.formattedBody - textFormat: TextEdit.RichText - readOnly: true - wrapMode: Text.Wrap width: parent ? parent.width : undefined - selectByMouse: true font.italic: true color: inactiveColors.text } diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml index 171bf18d..4c0e68c3 100644 --- a/resources/qml/delegates/Placeholder.qml +++ b/resources/qml/delegates/Placeholder.qml @@ -1,10 +1,7 @@ -import QtQuick 2.5 -import QtQuick.Controls 2.1 +import ".." -Label { +MatrixText { text: qsTr("unimplemented event: ") + model.type - textFormat: Text.PlainText - wrapMode: Text.Wrap width: parent ? parent.width : undefined color: inactiveColors.text } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 713be868..990a3f5b 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -1,11 +1,6 @@ -import QtQuick 2.5 +import ".." -TextEdit { +MatrixText { text: model.formattedBody - textFormat: TextEdit.RichText - readOnly: true - wrapMode: Text.Wrap width: parent ? parent.width : undefined - selectByMouse: true - color: colors.text } diff --git a/resources/res.qrc b/resources/res.qrc index 264ed82d..c9938d57 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -118,6 +118,7 @@ qml/TimelineView.qml qml/Avatar.qml qml/ImageButton.qml + qml/MatrixText.qml qml/StatusIndicator.qml qml/EncryptionIndicator.qml qml/TimelineRow.qml diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index fa87ec26..bdb3ea6f 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -827,6 +827,7 @@ TimelineModel::replyAction(QString id) [](const auto &e) -> std::string { return eventMsgType(e); }, event)); related.quoted_body = boost::apply_visitor([](const auto &e) -> QString { return eventBody(e); }, event); + related.room = room_id_; if (related.quoted_body.isEmpty()) return; From 1268e9f11c22e8cd22302342e80daef94b15001d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 5 Nov 2019 17:16:04 +0100 Subject: [PATCH 80/94] Make replies format nicer Also lays a bit of groundwork for better reply rendering --- resources/qml/TimelineRow.qml | 15 +++++++++-- src/Utils.cpp | 5 +--- src/timeline2/TimelineModel.cpp | 44 +++++++++++++++++++++++++-------- src/timeline2/TimelineModel.h | 1 + 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index c5c3fde0..8f9090e3 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -14,12 +14,23 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right - implicitHeight: contentItem.childrenRect.height + implicitHeight: contentItem.height - MessageDelegate { + Column { Layout.fillWidth: true Layout.alignment: Qt.AlignTop id: contentItem + + //property var replyTo: model.replyTo + + //Text { + // property int idx: timelineManager.timeline.idToIndex(replyTo) + // text: "" + (idx != -1 ? timelineManager.timeline.data(timelineManager.timeline.index(idx, 0), 2) : "nothing") + //} + MessageDelegate { + width: parent.width + height: childrenRect.height + } } StatusIndicator { diff --git a/src/Utils.cpp b/src/Utils.cpp index e27bc995..8f9e0643 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -366,7 +366,7 @@ utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html) { return QString("
In reply " - "to* %4
%4%5
") .arg(related.room, QString::fromStdString(related.related_event), @@ -382,9 +382,6 @@ utils::getQuoteBody(const RelatedInfo &related) using MsgType = mtx::events::MessageType; switch (related.type) { - case MsgType::Text: { - return markdownToHtml(related.quoted_body); - } case MsgType::File: { return QString(QCoreApplication::translate("utils", "sent a file.")); } diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index bdb3ea6f..b2b6f803 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -13,6 +13,8 @@ #include "Utils.h" #include "dialogs/RawMessage.h" +Q_DECLARE_METATYPE(QModelIndex) + namespace { template QString @@ -80,12 +82,6 @@ eventFormattedBody(const mtx::events::RoomEvent &e) { auto temp = e.content.formatted_body; if (!temp.empty()) { - auto pos = temp.find(""); - if (pos != std::string::npos) - temp.erase(pos, std::string("").size()); - pos = temp.find(""); - if (pos != std::string::npos) - temp.erase(pos, std::string("").size()); return QString::fromStdString(temp); } else { return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "
"); @@ -182,6 +178,21 @@ eventMimeType(const mtx::events::RoomEvent &e) return QString::fromStdString(e.content.info.mimetype); } +template +QString +eventRelatesTo(const mtx::events::Event &) +{ + return QString(); +} +template +auto +eventRelatesTo(const mtx::events::RoomEvent &e) -> std::enable_if_t< + std::is_same::value, + QString> +{ + return QString::fromStdString(e.content.relates_to.in_reply_to.event_id); +} + template qml_mtx_events::EventType toRoomEventType(const mtx::events::Event &e) @@ -383,6 +394,7 @@ TimelineModel::roleNames() const {Id, "id"}, {State, "state"}, {IsEncrypted, "isEncrypted"}, + {ReplyTo, "replyTo"}, }; } int @@ -450,8 +462,12 @@ TimelineModel::data(const QModelIndex &index, int role) const return QVariant(utils::replaceEmoji(boost::apply_visitor( [](const auto &e) -> QString { return eventBody(e); }, event))); case FormattedBody: - return QVariant(utils::replaceEmoji(boost::apply_visitor( - [](const auto &e) -> QString { return eventFormattedBody(e); }, event))); + return QVariant( + utils::replaceEmoji( + boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event)) + .remove("") + .remove("")); case Url: return QVariant(boost::apply_visitor( [](const auto &e) -> QString { return eventUrl(e); }, event)); @@ -501,6 +517,11 @@ TimelineModel::data(const QModelIndex &index, int role) const return boost::get>( &tempEvent) != nullptr; } + case ReplyTo: { + QString evId = boost::apply_visitor( + [](const auto &e) -> QString { return eventRelatesTo(e); }, event); + return QVariant(evId); + } default: return QVariant(); } @@ -825,8 +846,11 @@ TimelineModel::replyAction(QString id) event); related.type = mtx::events::getMessageType(boost::apply_visitor( [](const auto &e) -> std::string { return eventMsgType(e); }, event)); - related.quoted_body = - boost::apply_visitor([](const auto &e) -> QString { return eventBody(e); }, event); + related.quoted_body = boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event); + related.quoted_body.remove(QRegularExpression( + ".*", QRegularExpression::DotMatchesEverythingOption)); + nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString()); related.room = room_id_; if (related.quoted_body.isEmpty()) diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index 1ed6e72c..31e41315 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -139,6 +139,7 @@ public: Id, State, IsEncrypted, + ReplyTo, }; QHash roleNames() const override; From 2bfb885b4739dae46a35cfc5b1c62767b3900da9 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 8 Nov 2019 14:39:45 +0100 Subject: [PATCH 81/94] optionally use QQuickWidget and replace ColorOverlay -> colorImageProvider --- CMakeLists.txt | 4 +- resources/qml/EncryptionIndicator.qml | 8 +- resources/qml/ImageButton.qml | 9 +- resources/qml/StatusIndicator.qml | 15 +- resources/qml/TimelineRow.qml | 4 +- resources/qml/TimelineView.qml | 244 +++++++++++++------------- src/ColorImageProvider.cpp | 30 ++++ src/ColorImageProvider.h | 11 ++ src/timeline2/DelegateChooser.cpp | 1 - src/timeline2/TimelineModel.cpp | 2 +- src/timeline2/TimelineViewManager.cpp | 14 ++ src/timeline2/TimelineViewManager.h | 7 + 12 files changed, 197 insertions(+), 152 deletions(-) create mode 100644 src/ColorImageProvider.cpp create mode 100644 src/ColorImageProvider.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ae9a5e46..a7cddc50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,7 +69,7 @@ include(LMDB) # # Discover Qt dependencies. # -find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 REQUIRED) +find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED) find_package(Qt5QuickCompiler) find_package(Qt5DBus) @@ -234,6 +234,7 @@ set(SRC_FILES src/MainWindow.cpp src/MatrixClient.cpp src/MxcImageProvider.cpp + src/ColorImageProvider.cpp src/QuickSwitcher.cpp src/Olm.cpp src/RegisterPage.cpp @@ -414,6 +415,7 @@ set(COMMON_LIBS Qt5::Multimedia Qt5::Qml Qt5::QuickControls2 + Qt5::QuickWidgets nlohmann_json::nlohmann_json) if(APPVEYOR_BUILD) diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index 0d0e86cf..2cd9161b 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -1,6 +1,5 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 -import QtGraphicalEffects 1.0 import com.github.nheko 1.0 Rectangle { @@ -19,12 +18,7 @@ Rectangle { Image { id: stateImg anchors.fill: parent - source: "qrc:/icons/icons/ui/lock.png" - } - ColorOverlay { - anchors.fill: stateImg - source: stateImg - color: colors.buttonText + source: "image://colorimage/:/icons/icons/ui/lock.png?"+colors.buttonText } } diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml index dda9865b..dc576e18 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml @@ -1,9 +1,8 @@ import QtQuick 2.3 import QtQuick.Controls 2.3 -import QtGraphicalEffects 1.0 Button { - property alias image: buttonImg.source + property string image: undefined id: button @@ -17,11 +16,7 @@ Button { id: buttonImg // Workaround, can't get icon.source working for now... anchors.fill: parent - } - ColorOverlay { - anchors.fill: buttonImg - source: buttonImg - color: button.hovered ? colors.highlight : colors.buttonText + source: "image://colorimage/" + image + "?" + (button.hovered ? colors.highlight : colors.buttonText) } MouseArea diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 9f8d2cae..2ed59a17 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -1,6 +1,5 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 -import QtGraphicalEffects 1.0 import com.github.nheko 1.0 Rectangle { @@ -28,18 +27,12 @@ Rectangle { // Workaround, can't get icon.source working for now... anchors.fill: parent source: switch (indicator.state) { - case MtxEvent.Failed: return "qrc:/icons/icons/ui/remove-symbol.png" - case MtxEvent.Sent: return "qrc:/icons/icons/ui/clock.png" - case MtxEvent.Received: return "qrc:/icons/icons/ui/checkmark.png" - case MtxEvent.Read: return "qrc:/icons/icons/ui/double-tick-indicator.png" + case MtxEvent.Failed: return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText + case MtxEvent.Sent: return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText + case MtxEvent.Received: return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText + case MtxEvent.Read: return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText default: return "" } } - ColorOverlay { - anchors.fill: stateImg - source: stateImg - color: colors.buttonText - visible: stateImg.source != "" - } } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 8f9090e3..63a84701 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -50,7 +50,7 @@ RowLayout { Layout.preferredHeight: 16 id: replyButton - image: "qrc:/icons/icons/ui/mail-reply.png" + image: ":/icons/icons/ui/mail-reply.png" ToolTip { visible: replyButton.hovered text: qsTr("Reply") @@ -64,7 +64,7 @@ RowLayout { Layout.preferredHeight: 16 id: optionsButton - image: "qrc:/icons/icons/ui/vertical-ellipsis.png" + image: ":/icons/icons/ui/vertical-ellipsis.png" ToolTip { visible: optionsButton.hovered text: qsTr("Options") diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index c2f6f9b9..b25b3a7c 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -8,151 +8,151 @@ import com.github.nheko 1.0 import "./delegates" -Rectangle { - anchors.fill: parent - +Item { property var colors: currentActivePalette property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled } property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive property int avatarSize: 40 - color: colors.window - - Text { - visible: !timelineManager.timeline - anchors.centerIn: parent - text: qsTr("No room open") - font.pointSize: 24 - color: colors.windowText - } - - ListView { - id: chat - - cacheBuffer: 2000 - - visible: timelineManager.timeline != null + Rectangle { anchors.fill: parent + color: colors.window - anchors.leftMargin: 4 - anchors.rightMargin: scrollbar.width + Text { + visible: !timelineManager.timeline + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + color: colors.windowText + } - model: timelineManager.timeline + ListView { + id: chat - onModelChanged: { - if (model) { - currentIndex = model.currentIndex - if (model.currentIndex == count - 1) { + cacheBuffer: 2000 + + visible: timelineManager.timeline != null + anchors.fill: parent + + anchors.leftMargin: 4 + anchors.rightMargin: scrollbar.width + + model: timelineManager.timeline + + onModelChanged: { + if (model) { + currentIndex = model.currentIndex + if (model.currentIndex == count - 1) { + positionViewAtEnd() + } else { + positionViewAtIndex(model.currentIndex, ListView.End) + } + + //if (contentHeight < height) { + // model.fetchHistory(); + //} + } + } + + ScrollBar.vertical: ScrollBar { + id: scrollbar + anchors.top: parent.top + anchors.left: parent.right + anchors.bottom: parent.bottom + onPressedChanged: if (!pressed) chat.updatePosition() + } + + property bool atBottom: false + onCountChanged: { + if (atBottom) { + var newIndex = count - 1 // last index positionViewAtEnd() - } else { - positionViewAtIndex(model.currentIndex, ListView.End) + currentIndex = newIndex + model.currentIndex = newIndex } - if (contentHeight < height) { + if (contentHeight < height && model) { model.fetchHistory(); } } - } - ScrollBar.vertical: ScrollBar { - id: scrollbar - anchors.top: parent.top - anchors.left: parent.right - anchors.bottom: parent.bottom - onPressedChanged: if (!pressed) chat.updatePosition() - } + onAtYBeginningChanged: if (atYBeginning) model.fetchHistory() - property bool atBottom: false - onCountChanged: { - if (atBottom && Window.active) { - var newIndex = count - 1 // last index - positionViewAtEnd() - currentIndex = newIndex - model.currentIndex = newIndex - } - - if (contentHeight < height && model) { - model.fetchHistory(); - } - } - - onAtYBeginningChanged: if (atYBeginning) model.fetchHistory() - - function updatePosition() { - for (var y = chat.contentY + chat.height; y > chat.height; y -= 5) { - var i = chat.itemAt(100, y); - if (!i) continue; - if (!i.isFullyVisible()) continue; - chat.model.currentIndex = i.getIndex(); - chat.currentIndex = i.getIndex() - atBottom = i.getIndex() == count - 1; - console.log("bottom:" + atBottom) - break; - } - } - onMovementEnded: updatePosition() - - spacing: 4 - delegate: TimelineRow { - function isFullyVisible() { - return height > 1 && (y - chat.contentY - 1) + height < chat.height - } - function getIndex() { - return index; - } - } - - section { - property: "section" - delegate: Column { - topPadding: 4 - bottomPadding: 4 - spacing: 8 - - width: parent.width - height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 - - Label { - id: dateBubble - anchors.horizontalCenter: parent.horizontalCenter - visible: section.includes(" ") - text: chat.model.formatDateSeparator(new Date(Number(section.split(" ")[1]))) - color: colors.windowText - - height: contentHeight * 1.2 - width: contentWidth * 1.2 - horizontalAlignment: Text.AlignHCenter - background: Rectangle { - radius: parent.height / 2 - color: colors.dark - } + function updatePosition() { + for (var y = chat.contentY + chat.height; y > chat.height; y -= 9) { + var i = chat.itemAt(100, y); + if (!i) continue; + if (!i.isFullyVisible()) continue; + chat.model.currentIndex = i.getIndex(); + chat.currentIndex = i.getIndex() + atBottom = i.getIndex() == count - 1; + break; } - Row { - height: userName.height - spacing: 4 - Avatar { - width: avatarSize - height: avatarSize - url: chat.model.avatarUrl(section.split(" ")[0]).replace("mxc://", "image://MxcImage/") - displayName: chat.model.displayName(section.split(" ")[0]) + } + onMovementEnded: updatePosition() - MouseArea { - anchors.fill: parent - onClicked: chat.model.openUserProfile(section.split(" ")[0]) - cursorShape: Qt.PointingHandCursor + spacing: 4 + delegate: TimelineRow { + function isFullyVisible() { + return height > 1 && (y - chat.contentY - 1) + height < chat.height + } + function getIndex() { + return index; + } + } + + section { + property: "section" + delegate: Column { + topPadding: 4 + bottomPadding: 4 + spacing: 8 + + width: parent.width + height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 + + Label { + id: dateBubble + anchors.horizontalCenter: parent.horizontalCenter + visible: section.includes(" ") + text: chat.model.formatDateSeparator(new Date(Number(section.split(" ")[1]))) + color: colors.windowText + + height: contentHeight * 1.2 + width: contentWidth * 1.2 + horizontalAlignment: Text.AlignHCenter + background: Rectangle { + radius: parent.height / 2 + color: colors.dark } } + Row { + height: userName.height + spacing: 4 + Avatar { + width: avatarSize + height: avatarSize + url: chat.model.avatarUrl(section.split(" ")[0]).replace("mxc://", "image://MxcImage/") + displayName: chat.model.displayName(section.split(" ")[0]) - Text { - id: userName - text: chat.model.escapeEmoji(chat.model.displayName(section.split(" ")[0])) - color: chat.model.userColor(section.split(" ")[0], colors.window) - textFormat: Text.RichText + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(section.split(" ")[0]) + cursorShape: Qt.PointingHandCursor + } + } - MouseArea { - anchors.fill: parent - onClicked: chat.model.openUserProfile(section.split(" ")[0]) - 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) + textFormat: Text.RichText + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(section.split(" ")[0]) + cursorShape: Qt.PointingHandCursor + } } } } diff --git a/src/ColorImageProvider.cpp b/src/ColorImageProvider.cpp new file mode 100644 index 00000000..92e4732b --- /dev/null +++ b/src/ColorImageProvider.cpp @@ -0,0 +1,30 @@ +#include "ColorImageProvider.h" + +#include "Logging.h" +#include + +QPixmap +ColorImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &) +{ + auto args = id.split('?'); + + nhlog::ui()->info("Loading {}, source is {}", id.toStdString(), args[0].toStdString()); + + QPixmap source(args[0]); + + if (size) + *size = QSize(source.width(), source.height()); + + if (args.size() < 2) + return source; + + QColor color(args[1]); + + QPixmap colorized = source; + QPainter painter(&colorized); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(colorized.rect(), color); + painter.end(); + + return colorized; +} diff --git a/src/ColorImageProvider.h b/src/ColorImageProvider.h new file mode 100644 index 00000000..21f36c12 --- /dev/null +++ b/src/ColorImageProvider.h @@ -0,0 +1,11 @@ +#include + +class ColorImageProvider : public QQuickImageProvider +{ +public: + ColorImageProvider() + : QQuickImageProvider(QQuickImageProvider::Pixmap) + {} + + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; +}; diff --git a/src/timeline2/DelegateChooser.cpp b/src/timeline2/DelegateChooser.cpp index 6aeea69b..632a2a64 100644 --- a/src/timeline2/DelegateChooser.cpp +++ b/src/timeline2/DelegateChooser.cpp @@ -119,7 +119,6 @@ DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) chooser.child = dynamic_cast(object()); if (chooser.child == nullptr) { nhlog::ui()->error("Delegate has to be derived of Item!"); - delete chooser.child; return; } diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index b2b6f803..ab7d3d47 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -844,7 +844,7 @@ TimelineModel::replyAction(QString id) return related_; }, event); - related.type = mtx::events::getMessageType(boost::apply_visitor( + related.type = mtx::events::getMessageType(boost::apply_visitor( [](const auto &e) -> std::string { return eventMsgType(e); }, event)); related.quoted_body = boost::apply_visitor( [](const auto &e) -> QString { return eventFormattedBody(e); }, event); diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index a054bc78..d733ad90 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -8,6 +8,7 @@ #include #include "ChatPage.h" +#include "ColorImageProvider.h" #include "DelegateChooser.h" #include "Logging.h" #include "MxcImageProvider.h" @@ -51,6 +52,7 @@ TimelineViewManager::updateColorPalette() TimelineViewManager::TimelineViewManager(QWidget *parent) : imgProvider(new MxcImageProvider()) + , colorImgProvider(new ColorImageProvider()) { qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, "com.github.nheko", @@ -61,12 +63,24 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) qmlRegisterType("com.github.nheko", 1, 0, "DelegateChoice"); qmlRegisterType("com.github.nheko", 1, 0, "DelegateChooser"); +#ifdef USE_QUICK_VIEW view = new QQuickView(); container = QWidget::createWindowContainer(view, parent); +#else + view = new QQuickWidget(parent); + container = view; + view->setResizeMode(QQuickWidget::SizeRootObjectToView); + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { + nhlog::ui()->debug("Status changed to {}", status); + }); +#endif container->setMinimumSize(200, 200); view->rootContext()->setContextProperty("timelineManager", this); updateColorPalette(); view->engine()->addImageProvider("MxcImage", imgProvider); + view->engine()->addImageProvider("colorimage", colorImgProvider); view->setSource(QUrl("qrc:///qml/TimelineView.qml")); connect(dynamic_cast(parent), diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index b14e78ff..691c8ddb 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -16,6 +17,7 @@ #pragma GCC diagnostic ignored "-Wunused-parameter" class MxcImageProvider; +class ColorImageProvider; class TimelineViewManager : public QObject { @@ -99,10 +101,15 @@ public slots: uint64_t dsize); private: +#ifdef USE_QUICK_VIEW QQuickView *view; +#else + QQuickWidget *view; +#endif QWidget *container; TimelineModel *timeline_ = nullptr; MxcImageProvider *imgProvider; + ColorImageProvider *colorImgProvider; QHash> models; }; From 0cec167339aff5e7db43f64f6ab359f80df784fd Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 8 Nov 2019 22:08:51 +0100 Subject: [PATCH 82/94] Fix infinite item instantiating loop by using height instead of contentHeight --- resources/qml/TimelineRow.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 63a84701..a2d4fca5 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -14,12 +14,11 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right - implicitHeight: contentItem.height + height: Math.max(contentItem.height, 16) Column { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - id: contentItem //property var replyTo: model.replyTo @@ -28,6 +27,8 @@ RowLayout { // text: "" + (idx != -1 ? timelineManager.timeline.data(timelineManager.timeline.index(idx, 0), 2) : "nothing") //} MessageDelegate { + id: contentItem + width: parent.width height: childrenRect.height } From a3fc94496760b0e32f29d32bf81a172ac2161647 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 8 Nov 2019 22:29:47 +0100 Subject: [PATCH 83/94] Fix links opening user dialog and in browser --- resources/qml/MatrixText.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 5d20095c..46e74711 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -10,8 +10,8 @@ TextEdit { onLinkActivated: { if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]) - if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) - if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { + else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) + else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link) timelineManager.setHistoryView(match[1]) chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain) From e8f8182844a91a8ae5838f06a41968e95386904c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 8 Nov 2019 22:43:59 +0100 Subject: [PATCH 84/94] Use default macOS image --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 078611a0..b6112eb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,8 @@ matrix: include: - os: osx compiler: clang - osx_image: xcode9.2 + # Use the default osx image, because that one is actually tested to work with homebrew and probably the oldest supported version + # osx_image: xcode9 env: - DEPLOYMENT=1 - USE_BUNDLED_BOOST=0 From 91d1f19058a31cc35ca1212f042a9dd6f501a7b7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 9 Nov 2019 03:06:10 +0100 Subject: [PATCH 85/94] Remove old timeline --- CMakeLists.txt | 26 +- src/ChatPage.cpp | 2 +- src/Utils.h | 14 +- src/dialogs/MemberList.cpp | 1 + .../DelegateChooser.cpp | 0 src/{timeline2 => timeline}/DelegateChooser.h | 0 src/timeline/TimelineItem.cpp | 960 ---------- src/timeline/TimelineItem.h | 389 ---- src/{timeline2 => timeline}/TimelineModel.cpp | 0 src/{timeline2 => timeline}/TimelineModel.h | 0 src/timeline/TimelineView.cpp | 1627 ----------------- src/timeline/TimelineView.h | 449 ----- src/timeline/TimelineViewManager.cpp | 614 ++++--- src/timeline/TimelineViewManager.h | 107 +- src/timeline/widgets/AudioItem.cpp | 236 --- src/timeline/widgets/AudioItem.h | 104 -- src/timeline/widgets/FileItem.cpp | 221 --- src/timeline/widgets/FileItem.h | 79 - src/timeline/widgets/ImageItem.cpp | 267 --- src/timeline/widgets/ImageItem.h | 104 -- src/timeline/widgets/VideoItem.cpp | 65 - src/timeline/widgets/VideoItem.h | 51 - src/timeline2/TimelineViewManager.cpp | 400 ---- src/timeline2/TimelineViewManager.h | 117 -- 24 files changed, 413 insertions(+), 5420 deletions(-) rename src/{timeline2 => timeline}/DelegateChooser.cpp (100%) rename src/{timeline2 => timeline}/DelegateChooser.h (100%) delete mode 100644 src/timeline/TimelineItem.cpp delete mode 100644 src/timeline/TimelineItem.h rename src/{timeline2 => timeline}/TimelineModel.cpp (100%) rename src/{timeline2 => timeline}/TimelineModel.h (100%) delete mode 100644 src/timeline/TimelineView.cpp delete mode 100644 src/timeline/TimelineView.h delete mode 100644 src/timeline/widgets/AudioItem.cpp delete mode 100644 src/timeline/widgets/AudioItem.h delete mode 100644 src/timeline/widgets/FileItem.cpp delete mode 100644 src/timeline/widgets/FileItem.h delete mode 100644 src/timeline/widgets/ImageItem.cpp delete mode 100644 src/timeline/widgets/ImageItem.h delete mode 100644 src/timeline/widgets/VideoItem.cpp delete mode 100644 src/timeline/widgets/VideoItem.h delete mode 100644 src/timeline2/TimelineViewManager.cpp delete mode 100644 src/timeline2/TimelineViewManager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a7cddc50..e07df88d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,16 +192,9 @@ set(SRC_FILES src/emoji/Provider.cpp # Timeline - src/timeline2/TimelineViewManager.cpp - src/timeline2/TimelineModel.cpp - src/timeline2/DelegateChooser.cpp - #src/timeline/TimelineViewManager.cpp - #src/timeline/TimelineItem.cpp - #src/timeline/TimelineView.cpp - #src/timeline/widgets/AudioItem.cpp - #src/timeline/widgets/FileItem.cpp - #src/timeline/widgets/ImageItem.cpp - #src/timeline/widgets/VideoItem.cpp + src/timeline/TimelineViewManager.cpp + src/timeline/TimelineModel.cpp + src/timeline/DelegateChooser.cpp # UI components src/ui/Avatar.cpp @@ -339,16 +332,9 @@ qt5_wrap_cpp(MOC_HEADERS src/emoji/PickButton.h # Timeline - src/timeline2/TimelineViewManager.h - src/timeline2/TimelineModel.h - src/timeline2/DelegateChooser.h - #src/timeline/TimelineItem.h - #src/timeline/TimelineView.h - #src/timeline/TimelineViewManager.h - #src/timeline/widgets/AudioItem.h - #src/timeline/widgets/FileItem.h - #src/timeline/widgets/ImageItem.h - #src/timeline/widgets/VideoItem.h + src/timeline/TimelineViewManager.h + src/timeline/TimelineModel.h + src/timeline/DelegateChooser.h # UI components src/ui/Avatar.h diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index b8f312ac..091a9fa0 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -44,7 +44,7 @@ #include "dialogs/ReadReceipts.h" #include "popups/UserMentions.h" -#include "timeline2/TimelineViewManager.h" +#include "timeline/TimelineViewManager.h" // TODO: Needs to be updated with an actual secret. static const std::string STORAGE_SECRET_KEY("secret"); diff --git a/src/Utils.h b/src/Utils.h index 007126c3..bdb51844 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -4,10 +4,6 @@ #include "Cache.h" #include "RoomInfoListItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" #include #include @@ -94,7 +90,7 @@ messageDescription(const QString &username = "", using Video = mtx::events::RoomEvent; using Encrypted = mtx::events::EncryptedEvent; - if (std::is_same::value || std::is_same::value) { + if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent an audio clip"); @@ -102,7 +98,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent an audio clip") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent an image"); @@ -110,7 +106,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent an image") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a file"); @@ -118,7 +114,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent a file") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a video"); @@ -126,7 +122,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent a video") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a sticker"); diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index 9e973efa..f62cf9fe 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include diff --git a/src/timeline2/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp similarity index 100% rename from src/timeline2/DelegateChooser.cpp rename to src/timeline/DelegateChooser.cpp diff --git a/src/timeline2/DelegateChooser.h b/src/timeline/DelegateChooser.h similarity index 100% rename from src/timeline2/DelegateChooser.h rename to src/timeline/DelegateChooser.h diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp deleted file mode 100644 index 7916bd80..00000000 --- a/src/timeline/TimelineItem.cpp +++ /dev/null @@ -1,960 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#include - -#include -#include -#include -#include -#include -#include - -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "MainWindow.h" -#include "Olm.h" -#include "ui/Avatar.h" -#include "ui/Painter.h" -#include "ui/TextLabel.h" - -#include "timeline/TimelineItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -#include "dialogs/RawMessage.h" -#include "mtx/identifiers.hpp" - -constexpr int MSG_RIGHT_MARGIN = 7; -constexpr int MSG_PADDING = 20; - -StatusIndicator::StatusIndicator(QWidget *parent) - : QWidget(parent) -{ - lockIcon_.addFile(":/icons/icons/ui/lock.png"); - clockIcon_.addFile(":/icons/icons/ui/clock.png"); - checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png"); - doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png"); -} - -void -StatusIndicator::paintIcon(QPainter &p, QIcon &icon) -{ - auto pixmap = icon.pixmap(width()); - - QPainter painter(&pixmap); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(pixmap.rect(), p.pen().color()); - - QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal); -} - -void -StatusIndicator::paintEvent(QPaintEvent *) -{ - if (state_ == StatusIndicatorState::Empty) - return; - - Painter p(this); - PainterHighQualityEnabler hq(p); - - p.setPen(iconColor_); - - switch (state_) { - case StatusIndicatorState::Sent: { - paintIcon(p, clockIcon_); - break; - } - case StatusIndicatorState::Encrypted: - paintIcon(p, lockIcon_); - break; - case StatusIndicatorState::Received: { - paintIcon(p, checkmarkIcon_); - break; - } - case StatusIndicatorState::Read: { - paintIcon(p, doubleCheckmarkIcon_); - break; - } - case StatusIndicatorState::Empty: - break; - } -} - -void -StatusIndicator::setState(StatusIndicatorState state) -{ - state_ = state; - - switch (state) { - case StatusIndicatorState::Encrypted: - setToolTip(tr("Encrypted")); - break; - case StatusIndicatorState::Received: - setToolTip(tr("Delivered")); - break; - case StatusIndicatorState::Read: - setToolTip(tr("Seen")); - break; - case StatusIndicatorState::Sent: - setToolTip(tr("Sent")); - break; - case StatusIndicatorState::Empty: - setToolTip(""); - break; - } - - update(); -} - -void -TimelineItem::adjustMessageLayoutForWidget() -{ - messageLayout_->addLayout(widgetLayout_, 1); - actionLayout_->addWidget(replyBtn_); - actionLayout_->addWidget(contextBtn_); - messageLayout_->addLayout(actionLayout_); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight); - actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight); - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - messageLayout_->setAlignment(actionLayout_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::adjustMessageLayout() -{ - messageLayout_->addWidget(body_, 1); - actionLayout_->addWidget(replyBtn_); - actionLayout_->addWidget(contextBtn_); - messageLayout_->addLayout(actionLayout_); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight); - actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight); - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - messageLayout_->setAlignment(actionLayout_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::init() -{ - userAvatar_ = nullptr; - timestamp_ = nullptr; - userName_ = nullptr; - body_ = nullptr; - auto buttonSize_ = 32; - - contextMenu_ = new QMenu(this); - showReadReceipts_ = new QAction("Read receipts", this); - markAsRead_ = new QAction("Mark as read", this); - viewRawMessage_ = new QAction("View raw message", this); - redactMsg_ = new QAction("Redact message", this); - contextMenu_->addAction(showReadReceipts_); - contextMenu_->addAction(viewRawMessage_); - contextMenu_->addAction(markAsRead_); - contextMenu_->addAction(redactMsg_); - - connect(showReadReceipts_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - MainWindow::instance()->openReadReceiptsDialog(event_id_); - }); - - connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) { - emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id); - }); - connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) { - emit ChatPage::instance()->showNotification(msg); - }); - connect(redactMsg_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - http::client()->redact_event( - room_id_.toStdString(), - event_id_.toStdString(), - [this](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit redactionFailed(tr("Message redaction failed: %1") - .arg(QString::fromStdString( - err->matrix_error.error))); - return; - } - - emit eventRedacted(event_id_); - }); - }); - connect( - ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor); - connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt); - connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer); - - colorGenerating_ = new QFutureWatcher(this); - connect(colorGenerating_, - &QFutureWatcher::finished, - this, - &TimelineItem::finishedGeneratingColor); - - topLayout_ = new QHBoxLayout(this); - mainLayout_ = new QVBoxLayout; - messageLayout_ = new QHBoxLayout; - actionLayout_ = new QHBoxLayout; - messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0); - messageLayout_->setSpacing(MSG_PADDING); - - actionLayout_->setContentsMargins(13, 1, 13, 0); - actionLayout_->setSpacing(0); - - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0); - topLayout_->setSpacing(0); - topLayout_->addLayout(mainLayout_); - - mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0); - mainLayout_->setSpacing(0); - - replyBtn_ = new FlatButton(this); - replyBtn_->setToolTip(tr("Reply")); - replyBtn_->setFixedSize(buttonSize_, buttonSize_); - replyBtn_->setCornerRadius(buttonSize_ / 2); - - QIcon reply_icon; - reply_icon.addFile(":/icons/icons/ui/mail-reply.png"); - replyBtn_->setIcon(reply_icon); - replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); - connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction); - - contextBtn_ = new FlatButton(this); - contextBtn_->setToolTip(tr("Options")); - contextBtn_->setFixedSize(buttonSize_, buttonSize_); - contextBtn_->setCornerRadius(buttonSize_ / 2); - - QIcon context_icon; - context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png"); - contextBtn_->setIcon(context_icon); - contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); - contextBtn_->setMenu(contextMenu_); - - timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9); - timestampFont_.setFamily("Monospace"); - timestampFont_.setStyleHint(QFont::Monospace); - - QFontMetrics tsFm(timestampFont_); - - statusIndicator_ = new StatusIndicator(this); - statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading()); - statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading()); - - parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); -} - -/* - * For messages created locally. - */ -TimelineItem::TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(ty) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - // Generate the html body to be rendered. - auto formatted_body = utils::markdownToHtml(body); - - // Escape html if the input is not formatted. - if (formatted_body == body.trimmed().toHtmlEscaped()) - formatted_body = body.toHtmlEscaped(); - - QString emptyEventId; - - if (ty == mtx::events::MessageType::Emote) { - formatted_body = QString("%1").arg(formatted_body); - descriptionMsg_ = {emptyEventId, - "", - userid, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - } else { - descriptionMsg_ = {emptyEventId, - "You: ", - userid, - body, - utils::descriptiveTime(timestamp), - timestamp}; - } - - formatted_body = utils::linkifyMessage(formatted_body); - formatted_body.replace("mx-reply", "div"); - - generateTimestamp(timestamp); - - if (withSender) { - generateBody(userid, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(userid); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::TimelineItem(ImageItem *image, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Image) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(image, userid, withSender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::File) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(file, userid, withSender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Audio) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(audio, userid, withSender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Video) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(video, userid, withSender); -} - -TimelineItem::TimelineItem(ImageItem *image, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Image) - , room_id_{room_id} -{ - setupWidgetLayout, ImageItem>( - image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(StickerItem *image, - const mtx::events::Sticker &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout(image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::File) - , room_id_{room_id} -{ - setupWidgetLayout, FileItem>( - file, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Audio) - , room_id_{room_id} -{ - setupWidgetLayout, AudioItem>( - audio, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Video) - , room_id_{room_id} -{ - setupWidgetLayout, VideoItem>( - video, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -/* - * Used to display remote notice messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Notice) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - descriptionMsg_ = {event_id_, - Cache::displayName(room_id_, sender), - sender, - " sent a notification", - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - auto displayName = Cache::displayName(room_id_, sender); - - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote emote messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Emote) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - formatted_body = QString("%1").arg(formatted_body); - - descriptionMsg_ = {event_id_, - "", - sender, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote text messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Text) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(": %1").arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::~TimelineItem() -{ - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); -} - -void -TimelineItem::markSent() -{ - statusIndicator_->setState(StatusIndicatorState::Sent); -} - -void -TimelineItem::markOwnMessagesAsReceived(const std::string &sender) -{ - QSettings settings; - if (sender == settings.value("auth/user_id").toString().toStdString()) - statusIndicator_->setState(StatusIndicatorState::Received); -} - -void -TimelineItem::markRead() -{ - if (statusIndicator_->state() != StatusIndicatorState::Encrypted) - statusIndicator_->setState(StatusIndicatorState::Read); -} - -void -TimelineItem::markReceived(bool isEncrypted) -{ - isReceived_ = true; - - if (isEncrypted) - statusIndicator_->setState(StatusIndicatorState::Encrypted); - else - statusIndicator_->setState(StatusIndicatorState::Received); - - sendReadReceipt(); -} - -// Only the body is displayed. -void -TimelineItem::generateBody(const QString &body) -{ - body_ = new TextLabel(utils::replaceEmoji(body), this); - body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - - connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) { - MainWindow::instance()->openUserProfile(user_id, - ChatPage::instance()->currentRoom()); - }); -} - -void -TimelineItem::refreshAuthorColor() -{ - // Cancel and wait if we are already generating the color. - if (colorGenerating_->isRunning()) { - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); - } - if (userName_) { - // generate user's unique color. - std::function generate = [this]() { - QString userColor = utils::generateContrastingHexColor( - userName_->toolTip(), backgroundColor().name()); - return userColor; - }; - - QString userColor = Cache::userColor(userName_->toolTip()); - - // If the color is empty, then generate it asynchronously - if (userColor.isEmpty()) { - colorGenerating_->setFuture(QtConcurrent::run(generate)); - } else { - userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); - } - } -} - -void -TimelineItem::finishedGeneratingColor() -{ - nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString()); - QString userColor = colorGenerating_->result(); - - if (!userColor.isEmpty()) { - // another TimelineItem might have inserted in the meantime. - if (Cache::userColor(userName_->toolTip()).isEmpty()) { - Cache::insertUserColor(userName_->toolTip(), userColor); - } - userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); - } -} -// The username/timestamp is displayed along with the message body. -void -TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body) -{ - generateUserName(user_id, displayname); - generateBody(body); -} - -void -TimelineItem::generateUserName(const QString &user_id, const QString &displayname) -{ - auto sender = displayname; - - if (displayname.startsWith("@")) { - // TODO: Fix this by using a UserId type. - if (displayname.split(":")[0].split("@").size() > 1) - sender = displayname.split(":")[0].split("@")[1]; - } - - QFont usernameFont; - usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1); - usernameFont.setWeight(QFont::Medium); - - QFontMetrics fm(usernameFont); - - userName_ = new QLabel(this); - userName_->setFont(usernameFont); - userName_->setText(utils::replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500))); - userName_->setToolTip(user_id); - userName_->setToolTipDuration(1500); - userName_->setAttribute(Qt::WA_Hover); - userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - // width deprecated in 5.13: - userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text())); -#else - userName_->setFixedWidth( - QFontMetrics(userName_->font()).horizontalAdvance(userName_->text())); -#endif - // Set the user color asynchronously if it hasn't been generated yet, - // otherwise this will just set it. - refreshAuthorColor(); - - auto filter = new UserProfileFilter(user_id, userName_); - userName_->installEventFilter(filter); - userName_->setCursor(Qt::PointingHandCursor); - - connect(filter, &UserProfileFilter::hoverOn, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(true); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::hoverOff, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(false); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() { - MainWindow::instance()->openUserProfile(user_id, room_id_); - }); -} - -void -TimelineItem::generateTimestamp(const QDateTime &time) -{ - timestamp_ = new QLabel(this); - timestamp_->setFont(timestampFont_); - timestamp_->setText( - QString(" %1 ").arg(time.toString("HH:mm"))); -} - -void -TimelineItem::setupAvatarLayout(const QString &userName) -{ - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0); - - QFont f; - f.setPointSizeF(f.pointSizeF()); - - userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2); - userAvatar_->setLetter(QChar(userName[0]).toUpper()); - - // TODO: The provided user name should be a UserId class - if (userName[0] == '@' && userName.size() > 1) - userAvatar_->setLetter(QChar(userName[1]).toUpper()); - - topLayout_->insertWidget(0, userAvatar_); - topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft); - - if (userName_) - mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft); -} - -void -TimelineItem::setupSimpleLayout() -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - - topLayout_->setContentsMargins(conf::timeline::msgLeftMargin + - QFontMetrics(f).height() * 2 + 2, - conf::timeline::msgTopMargin, - 0, - 0); -} - -void -TimelineItem::setUserAvatar(const QString &userid) -{ - if (userAvatar_ == nullptr) - return; - - userAvatar_->setImage(room_id_, userid); -} - -void -TimelineItem::contextMenuEvent(QContextMenuEvent *event) -{ - if (contextMenu_) - contextMenu_->exec(event->globalPos()); -} - -void -TimelineItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineItem::addSaveImageAction(ImageItem *image) -{ - if (contextMenu_) { - auto saveImage = new QAction("Save image", this); - contextMenu_->addAction(saveImage); - - connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs); - } -} - -void -TimelineItem::addReplyAction() -{ - if (contextMenu_) { - auto replyAction = new QAction("Reply", this); - contextMenu_->addAction(replyAction); - - connect(replyAction, &QAction::triggered, this, &TimelineItem::replyAction); - } -} - -void -TimelineItem::replyAction() -{ - if (!body_) - return; - - RelatedInfo related; - related.type = message_type_; - related.quoted_body = body_->toPlainText(); - related.quoted_user = descriptionMsg_.userid; - related.related_event = eventId().toStdString(); - related.room = room_id_; - - emit ChatPage::instance()->messageReply(related); -} - -void -TimelineItem::addKeyRequestAction() -{ - if (contextMenu_) { - auto requestKeys = new QAction("Request encryption keys", this); - contextMenu_->addAction(requestKeys); - - connect(requestKeys, &QAction::triggered, this, [this]() { - olm::request_keys(room_id_.toStdString(), event_id_.toStdString()); - }); - } -} - -void -TimelineItem::addAvatar() -{ - if (userAvatar_) - return; - - // TODO: should be replaced with the proper event struct. - auto userid = descriptionMsg_.userid; - auto displayName = Cache::displayName(room_id_, userid); - - generateUserName(userid, displayName); - - setupAvatarLayout(displayName); - - setUserAvatar(userid); -} - -void -TimelineItem::sendReadReceipt() const -{ - if (!event_id_.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - event_id_.toStdString(), - [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read_event ({}, {})", - room_id_.toStdString(), - event_id_.toStdString()); - } - }); -} - -void -TimelineItem::openRawMessageViewer() const -{ - const auto event_id = event_id_.toStdString(); - const auto room_id = room_id_.toStdString(); - - auto proxy = std::make_shared(); - connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) { - auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))}; - Q_UNUSED(dialog); - }); - - http::client()->get_event( - room_id, - event_id, - [event_id, room_id, proxy = std::move(proxy)]( - const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) { - using namespace mtx::events; - - if (err) { - nhlog::net()->warn( - "failed to retrieve event {} from {}", event_id, room_id); - return; - } - - try { - emit proxy->eventRetrieved(utils::serialize_event(res)); - } catch (const nlohmann::json::exception &e) { - nhlog::net()->warn( - "failed to serialize event ({}, {})", room_id, event_id); - } - }); -} diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h deleted file mode 100644 index 356976e5..00000000 --- a/src/timeline/TimelineItem.h +++ /dev/null @@ -1,389 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "mtx/events.hpp" - -#include "AvatarProvider.h" -#include "RoomInfoListItem.h" -#include "Utils.h" - -#include "Cache.h" -#include "MatrixClient.h" - -#include "ui/FlatButton.h" - -class ImageItem; -class StickerItem; -class AudioItem; -class VideoItem; -class FileItem; -class Avatar; -class TextLabel; - -enum class StatusIndicatorState -{ - //! The encrypted message was received by the server. - Encrypted, - //! 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, -}; - -//! -//! Used to notify the user about the status of a message. -//! -class StatusIndicator : public QWidget -{ - Q_OBJECT - -public: - explicit StatusIndicator(QWidget *parent); - void setState(StatusIndicatorState state); - StatusIndicatorState state() const { return state_; } - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void paintIcon(QPainter &p, QIcon &icon); - - QIcon lockIcon_; - QIcon clockIcon_; - QIcon checkmarkIcon_; - QIcon doubleCheckmarkIcon_; - - QColor iconColor_ = QColor("#999"); - - StatusIndicatorState state_ = StatusIndicatorState::Empty; - - static constexpr int MaxWidth = 24; -}; - -class EventProxy : public QObject -{ - Q_OBJECT - -signals: - void eventRetrieved(const nlohmann::json &); -}; - -class UserProfileFilter : public QObject -{ - Q_OBJECT - -public: - explicit UserProfileFilter(const QString &user_id, QLabel *parent) - : QObject(parent) - , user_id_{user_id} - {} - -signals: - void hoverOff(); - void hoverOn(); - void clicked(); - -protected: - bool eventFilter(QObject *obj, QEvent *event) - { - if (event->type() == QEvent::MouseButtonRelease) { - emit clicked(); - return true; - } else if (event->type() == QEvent::HoverLeave) { - emit hoverOff(); - return true; - } else if (event->type() == QEvent::HoverEnter) { - emit hoverOn(); - return true; - } - - return QObject::eventFilter(obj, event); - } - -private: - QString user_id_; -}; - -class TimelineItem : public QWidget -{ - Q_OBJECT - Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) - -public: - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - - // For local messages. - // m.text & m.emote - TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - // m.image - TimelineItem(ImageItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(FileItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(AudioItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(VideoItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - - TimelineItem(ImageItem *img, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(StickerItem *img, - const mtx::events::Sticker &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(FileItem *file, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(VideoItem *video, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - - ~TimelineItem(); - - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - QColor backgroundColor() const { return backgroundColor_; } - - void setUserAvatar(const QString &userid); - DescInfo descriptionMessage() const { return descriptionMsg_; } - QString eventId() const { return event_id_; } - void setEventId(const QString &event_id) { event_id_ = event_id; } - void markReceived(bool isEncrypted); - void markRead(); - void markSent(); - bool isReceived() { return isReceived_; }; - void setRoomId(QString room_id) { room_id_ = room_id; } - void sendReadReceipt() const; - void openRawMessageViewer() const; - void replyAction(); - - //! Add a user avatar for this event. - void addAvatar(); - void addKeyRequestAction(); - -signals: - void eventRedacted(const QString &event_id); - void redactionFailed(const QString &msg); - -public slots: - void refreshAuthorColor(); - void finishedGeneratingColor(); - -protected: - void paintEvent(QPaintEvent *event) override; - void contextMenuEvent(QContextMenuEvent *event) override; - -private: - //! If we are the sender of the message the event wil be marked as received by the server. - void markOwnMessagesAsReceived(const std::string &sender); - void init(); - //! Add a context menu option to save the image of the timeline item. - void addSaveImageAction(ImageItem *image); - //! Add the reply action in the context menu for widgets that support it. - void addReplyAction(); - - template - void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender); - - template - void setupWidgetLayout(Widget *widget, const Event &event, bool withSender); - - void generateBody(const QString &body); - void generateBody(const QString &user_id, const QString &displayname, const QString &body); - void generateTimestamp(const QDateTime &time); - void generateUserName(const QString &userid, const QString &displayname); - - void setupAvatarLayout(const QString &userName); - void setupSimpleLayout(); - - void adjustMessageLayout(); - void adjustMessageLayoutForWidget(); - - //! Whether or not the event associated with the widget - //! has been acknowledged by the server. - bool isReceived_ = false; - - QFutureWatcher *colorGenerating_; - - QString event_id_; - mtx::events::MessageType message_type_ = mtx::events::MessageType::Unknown; - QString room_id_; - - DescInfo descriptionMsg_; - - QMenu *contextMenu_; - QAction *showReadReceipts_; - QAction *markAsRead_; - QAction *redactMsg_; - QAction *viewRawMessage_; - QAction *replyMsg_; - - QHBoxLayout *topLayout_ = nullptr; - QHBoxLayout *messageLayout_ = nullptr; - QHBoxLayout *actionLayout_ = nullptr; - QVBoxLayout *mainLayout_ = nullptr; - QHBoxLayout *widgetLayout_ = nullptr; - - Avatar *userAvatar_; - - QFont timestampFont_; - - StatusIndicator *statusIndicator_; - - QLabel *timestamp_; - QLabel *userName_; - TextLabel *body_; - - QColor backgroundColor_; - - FlatButton *replyBtn_; - FlatButton *contextBtn_; -}; - -template -void -TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender) -{ - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - descriptionMsg_ = {"", // No event_id up until this point. - "You", - userid, - QString(" %1").arg(utils::messageDescription()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout; - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(userid, displayName, ""); - setupAvatarLayout(displayName); - - setUserAvatar(userid); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} - -template -void -TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender) -{ - init(); - - // if (event.type == mtx::events::EventType::RoomMessage) { - // message_type_ = mtx::events::getMessageType(event.content.msgtype); - //} - // TODO: Fix this. - message_type_ = mtx::events::MessageType::Unknown; - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(" %1").arg(utils::messageDescription()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout(); - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(sender, displayName, ""); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline/TimelineModel.cpp similarity index 100% rename from src/timeline2/TimelineModel.cpp rename to src/timeline/TimelineModel.cpp diff --git a/src/timeline2/TimelineModel.h b/src/timeline/TimelineModel.h similarity index 100% rename from src/timeline2/TimelineModel.h rename to src/timeline/TimelineModel.h diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp deleted file mode 100644 index ed783e90..00000000 --- a/src/timeline/TimelineView.cpp +++ /dev/null @@ -1,1627 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include - -#include -#include -#include -#include - -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "Olm.h" -#include "UserSettingsPage.h" -#include "Utils.h" -#include "ui/FloatingButton.h" -#include "ui/InfoMessage.h" - -#include "timeline/TimelineView.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -using TimelineEvent = mtx::events::collections::TimelineEvents; - -//! Maximum number of widgets to keep in the timeline layout. -constexpr int MAX_RETAINED_WIDGETS = 100; -constexpr int MIN_SCROLLBAR_HANDLE = 60; - -//! Retrieve the timestamp of the event represented by the given widget. -QDateTime -getDate(QWidget *widget) -{ - auto item = qobject_cast(widget); - if (item) - return item->descriptionMessage().datetime; - - auto infoMsg = qobject_cast(widget); - if (infoMsg) - return infoMsg->datetime(); - - return QDateTime(); -} - -TimelineView::TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addEvents(timeline); -} - -TimelineView::TimelineView(const QString &room_id, QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - getMessages(); -} - -void -TimelineView::sliderRangeChanged(int min, int max) -{ - Q_UNUSED(min); - - if (!scroll_area_->verticalScrollBar()->isVisible()) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - // If the scrollbar is close to the bottom and a new message - // is added we move the scrollbar. - if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - int currentHeight = scroll_widget_->size().height(); - int diff = currentHeight - oldHeight_; - int newPosition = oldPosition_ + diff; - - // Keep the scroll bar to the bottom if it hasn't been activated yet. - if (oldPosition_ == 0 && !scroll_area_->verticalScrollBar()->isVisible()) - newPosition = max; - - if (lastMessageDirection_ == TimelineDirection::Top) - scroll_area_->verticalScrollBar()->setValue(newPosition); -} - -void -TimelineView::fetchHistory() -{ - if (!isScrollbarActivated() && !isTimelineFinished) { - if (!isVisible()) - return; - - isPaginationInProgress_ = true; - getMessages(); - paginationTimer_->start(2000); - - return; - } - - paginationTimer_->stop(); -} - -void -TimelineView::scrollDown() -{ - int current = scroll_area_->verticalScrollBar()->value(); - int max = scroll_area_->verticalScrollBar()->maximum(); - - // The first time we enter the room move the scroll bar to the bottom. - if (!isInitialized) { - scroll_area_->verticalScrollBar()->setValue(max); - isInitialized = true; - return; - } - - // If the gap is small enough move the scroll bar down. e.g when a new - // message appears. - if (max - current < SCROLL_BAR_GAP) - scroll_area_->verticalScrollBar()->setValue(max); -} - -void -TimelineView::sliderMoved(int position) -{ - if (!scroll_area_->verticalScrollBar()->isVisible()) - return; - - toggleScrollDownButton(); - - // The scrollbar is high enough so we can start retrieving old events. - if (position < SCROLL_BAR_GAP) { - if (isTimelineFinished) - return; - - // Prevent user from moving up when there is pagination in - // progress. - if (isPaginationInProgress_) - return; - - isPaginationInProgress_ = true; - - getMessages(); - } -} - -bool -TimelineView::isStartOfTimeline(const mtx::responses::Messages &msgs) -{ - return (msgs.chunk.size() == 0 && (msgs.end.empty() || msgs.end == msgs.start)); -} - -void -TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs) -{ - // We've reached the start of the timline and there're no more messages. - if (isStartOfTimeline(msgs)) { - nhlog::ui()->info("[{}] start of timeline reached, no more messages to fetch", - room_id_.toStdString()); - isTimelineFinished = true; - return; - } - - isTimelineFinished = false; - - // Queue incoming messages to be rendered later. - topMessages_.insert(topMessages_.end(), - std::make_move_iterator(msgs.chunk.begin()), - std::make_move_iterator(msgs.chunk.end())); - - // The RoomList message preview will be updated only if this - // is the first batch of messages received through /messages - // i.e there are no other messages currently present. - if (!topMessages_.empty() && scroll_layout_->count() == 0) - notifyForLastEvent(findFirstViewableEvent(topMessages_)); - - if (isVisible()) { - renderTopEvents(topMessages_); - - // Free up space for new messages. - topMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } - - prev_batch_token_ = QString::fromStdString(msgs.end); - isPaginationInProgress_ = false; -} - -QWidget * -TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction) -{ - using namespace mtx::events; - - using AudioEvent = RoomEvent; - using EmoteEvent = RoomEvent; - using FileEvent = RoomEvent; - using ImageEvent = RoomEvent; - using NoticeEvent = RoomEvent; - using TextEvent = RoomEvent; - using VideoEvent = RoomEvent; - - if (boost::get>(&event) != nullptr) { - auto redaction_event = boost::get>(event); - const auto event_id = QString::fromStdString(redaction_event.redacts); - - QTimer::singleShot(0, this, [event_id, this]() { - if (eventIds_.contains(event_id)) - removeEvent(event_id); - }); - - return nullptr; - } else if (boost::get>(&event) != nullptr) { - auto msg = boost::get>(event); - auto event_id = QString::fromStdString(msg.event_id); - - if (eventIds_.contains(event_id)) - return nullptr; - - auto item = new InfoMessage(tr("Encryption is enabled"), this); - item->saveDatetime(QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts)); - eventIds_[event_id] = item; - - // Force the next message to have avatar by not providing the current username. - saveMessageInfo("", msg.origin_server_ts, direction); - - return item; - } else if (boost::get>(&event) != nullptr) { - auto audio = boost::get>(event); - return processMessageEvent(audio, direction); - } else if (boost::get>(&event) != nullptr) { - auto emote = boost::get>(event); - return processMessageEvent(emote, direction); - } else if (boost::get>(&event) != nullptr) { - auto file = boost::get>(event); - return processMessageEvent(file, direction); - } else if (boost::get>(&event) != nullptr) { - auto image = boost::get>(event); - return processMessageEvent(image, direction); - } else if (boost::get>(&event) != nullptr) { - auto notice = boost::get>(event); - return processMessageEvent(notice, direction); - } else if (boost::get>(&event) != nullptr) { - auto text = boost::get>(event); - return processMessageEvent(text, direction); - } else if (boost::get>(&event) != nullptr) { - auto video = boost::get>(event); - return processMessageEvent(video, direction); - } else if (boost::get(&event) != nullptr) { - return processMessageEvent(boost::get(event), - direction); - } else if (boost::get>(&event) != nullptr) { - auto res = parseEncryptedEvent(boost::get>(event)); - auto widget = parseMessageEvent(res.event, direction); - - if (widget == nullptr) - return nullptr; - - auto item = qobject_cast(widget); - - if (item && res.isDecrypted) - item->markReceived(true); - else if (item && !res.isDecrypted) - item->addKeyRequestAction(); - - return widget; - } - - return nullptr; -} - -DecryptionResult -TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent &e) -{ - MegolmSessionIndex index; - index.room_id = room_id_.toStdString(); - index.session_id = e.content.session_id; - index.sender_key = e.content.sender_key; - - mtx::events::RoomEvent dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - dummy.content.body = - tr("-- Encrypted Event (No keys found for decryption) --", - "Placeholder, when the message was not decrypted yet or can't be decrypted") - .toStdString(); - - try { - if (!cache::client()->inboundMegolmSessionExists(index)) { - nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", - index.room_id, - index.session_id, - e.sender); - // TODO: request megolm session_id & session_key from the sender. - return {dummy, false}; - } - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); - dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", - "Placeholder, when the message can't be decrypted, because " - "the DB access failed when trying to lookup the session.") - .toStdString(); - return {dummy, false}; - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB access " - "failed.") - .toStdString(); - return {dummy, false}; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the Olm " - "decrytion returned an error, which is passed ad %1") - .arg(e.what()) - .toStdString(); - return {dummy, false}; - } - - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = e.event_id; - body["sender"] = e.sender; - body["origin_server_ts"] = e.origin_server_ts; - body["unsigned"] = e.unsigned_data; - - nhlog::crypto()->debug("decrypted event: {}", e.event_id); - - json event_array = json::array(); - event_array.push_back(body); - - std::vector events; - mtx::responses::utils::parse_timeline_events(event_array, events); - - if (events.size() == 1) - return {events.at(0), true}; - - dummy.content.body = - tr("-- 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") - .toStdString(); - return {dummy, false}; -} - -void -TimelineView::displayReadReceipts(std::vector events) -{ - QtConcurrent::run( - [events = std::move(events), room_id = room_id_, local_user = local_user_, this]() { - std::vector event_ids; - - for (const auto &e : events) { - if (utils::event_sender(e) == local_user) - event_ids.emplace_back( - QString::fromStdString(utils::event_id(e))); - } - - auto readEvents = - cache::client()->filterReadEvents(room_id, event_ids, local_user.toStdString()); - - if (!readEvents.empty()) - emit markReadEvents(readEvents); - }); -} - -void -TimelineView::renderBottomEvents(const std::vector &events) -{ - int counter = 0; - - for (const auto &event : events) { - QWidget *item = parseMessageEvent(event, TimelineDirection::Bottom); - - if (item != nullptr) { - addTimelineItem(item, TimelineDirection::Bottom); - counter++; - - // Prevent blocking of the event-loop - // by calling processEvents every 10 items we render. - if (counter % 4 == 0) - QApplication::processEvents(); - } - } - - lastMessageDirection_ = TimelineDirection::Bottom; - - displayReadReceipts(events); - - QApplication::processEvents(); -} - -void -TimelineView::renderTopEvents(const std::vector &events) -{ - std::vector items; - - // Reset the sender of the first message in the timeline - // cause we're about to insert a new one. - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - - // Parse in reverse order to determine where we should not show sender's name. - for (auto it = events.rbegin(); it != events.rend(); ++it) { - auto item = parseMessageEvent(*it, TimelineDirection::Top); - - if (item != nullptr) - items.push_back(item); - } - - // Reverse again to render them. - std::reverse(items.begin(), items.end()); - - oldPosition_ = scroll_area_->verticalScrollBar()->value(); - oldHeight_ = scroll_widget_->size().height(); - - for (const auto &item : items) - addTimelineItem(item, TimelineDirection::Top); - - lastMessageDirection_ = TimelineDirection::Top; - - QApplication::processEvents(); - - displayReadReceipts(events); - - // If this batch is the first being rendered (i.e the first and the last - // events originate from this batch), set the last sender. - if (lastSender_.isEmpty() && !items.empty()) { - for (const auto &w : items) { - auto timelineItem = qobject_cast(w); - if (timelineItem) { - saveLastMessageInfo(timelineItem->descriptionMessage().userid, - timelineItem->descriptionMessage().datetime); - break; - } - } - } -} - -void -TimelineView::addEvents(const mtx::responses::Timeline &timeline) -{ - if (isInitialSync) { - prev_batch_token_ = QString::fromStdString(timeline.prev_batch); - isInitialSync = false; - } - - bottomMessages_.insert(bottomMessages_.end(), - std::make_move_iterator(timeline.events.begin()), - std::make_move_iterator(timeline.events.end())); - - if (!bottomMessages_.empty()) - notifyForLastEvent(findLastViewableEvent(bottomMessages_)); - - // If the current timeline is open and there are messages to be rendered. - if (isVisible() && !bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - - // Free up space for new messages. - bottomMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } -} - -void -TimelineView::init() -{ - local_user_ = utils::localUser(); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-arrow-down.png"); - scrollDownBtn_ = new FloatingButton(icon, this); - scrollDownBtn_->hide(); - - connect(scrollDownBtn_, &QPushButton::clicked, this, [this]() { - const int max = scroll_area_->verticalScrollBar()->maximum(); - scroll_area_->verticalScrollBar()->setValue(max); - }); - top_layout_ = new QVBoxLayout(this); - top_layout_->setSpacing(0); - top_layout_->setMargin(0); - - scroll_area_ = new QScrollArea(this); - scroll_area_->setWidgetResizable(true); - scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scroll_widget_ = new QWidget(this); - scroll_widget_->setObjectName("scroll_widget"); - - // Height of the typing display. - QFont f; - f.setPointSizeF(f.pointSizeF() * 0.9); - const int bottomMargin = QFontMetrics(f).height() + 6; - - scroll_layout_ = new QVBoxLayout(scroll_widget_); - scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); - scroll_layout_->setSpacing(0); - scroll_layout_->setObjectName("timelinescrollarea"); - - scroll_area_->setWidget(scroll_widget_); - scroll_area_->setAlignment(Qt::AlignBottom); - - top_layout_->addWidget(scroll_area_); - - setLayout(top_layout_); - - paginationTimer_ = new QTimer(this); - connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory); - - connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents); - - connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage); - connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage); - - connect( - this, &TimelineView::markReadEvents, this, [this](const std::vector &event_ids) { - for (const auto &event : event_ids) { - if (eventIds_.contains(event)) { - auto widget = eventIds_[event]; - if (!widget) - return; - - auto item = qobject_cast(widget); - if (!item) - return; - - item->markRead(); - } - } - }); - - connect(scroll_area_->verticalScrollBar(), - SIGNAL(valueChanged(int)), - this, - SLOT(sliderMoved(int))); - connect(scroll_area_->verticalScrollBar(), - SIGNAL(rangeChanged(int, int)), - this, - SLOT(sliderRangeChanged(int, int))); -} - -void -TimelineView::getMessages() -{ - mtx::http::MessagesOpts opts; - opts.room_id = room_id_.toStdString(); - opts.from = prev_batch_token_.toStdString(); - - 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); - return; - } - - emit messagesRetrieved(std::move(res)); - }); -} - -void -TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) - lastSender_ = user_id; - else - firstSender_ = user_id; -} - -bool -TimelineView::isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) { - return (lastSender_ != user_id) || - isDateDifference(lastMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } else { - return (firstSender_ != user_id) || - isDateDifference(firstMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } -} - -void -TimelineView::addTimelineItem(QWidget *item, TimelineDirection direction) -{ - const auto newDate = getDate(item); - - if (direction == TimelineDirection::Bottom) { - QWidget *lastItem = nullptr; - int lastItemPosition = 0; - - if (scroll_layout_->count() > 0) { - lastItemPosition = scroll_layout_->count() - 1; - lastItem = scroll_layout_->itemAt(lastItemPosition)->widget(); - } - - if (lastItem) { - const auto oldDate = getDate(lastItem); - - if (oldDate.daysTo(newDate) != 0) { - auto separator = new DateSeparator(newDate, this); - - if (separator) - pushTimelineItem(separator, direction); - } - } - - pushTimelineItem(item, direction); - } else { - if (scroll_layout_->count() > 0) { - const auto firstItem = scroll_layout_->itemAt(0)->widget(); - - if (firstItem) { - const auto oldDate = getDate(firstItem); - - if (newDate.daysTo(oldDate) != 0) { - auto separator = new DateSeparator(oldDate); - - if (separator) - pushTimelineItem(separator, direction); - } - } - } - - pushTimelineItem(item, direction); - } -} - -void -TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id) -{ - nhlog::ui()->debug("[{}] message was received by the server", txn_id); - if (!pending_msgs_.isEmpty() && - pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet - auto msg = pending_msgs_.dequeue(); - msg.event_id = event_id; - - if (msg.widget) { - msg.widget->setEventId(event_id); - eventIds_[event_id] = msg.widget; - - // If the response comes after we have received the event from sync - // we've already marked the widget as received. - if (!msg.widget->isReceived()) { - msg.widget->markReceived(msg.is_encrypted); - cache::client()->addPendingReceipt(room_id_, event_id); - pending_sent_msgs_.append(msg); - } - } else { - nhlog::ui()->warn("[{}] received message response for invalid widget", - txn_id); - } - } - - sendNextPendingMessage(); -} - -void -TimelineView::addUserMessage(mtx::events::MessageType ty, - const QString &body, - const RelatedInfo &related = RelatedInfo()) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - - QString full_body; - if (related.related_event.empty()) { - full_body = body; - } else { - full_body = utils::getFormattedQuoteBody(related, body); - } - TimelineItem *view_item = - new TimelineItem(ty, local_user_, full_body, with_sender, room_id_, scroll_widget_); - - PendingMessage message; - message.ty = ty; - message.txn_id = http::client()->generate_txn_id(); - message.body = body; - message.related = related; - message.widget = view_item; - - try { - message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check encryption status of room {}", e.what()); - view_item->deleteLater(); - - // TODO: Send a notification to the user. - - return; - } - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - handleNewUserMessage(message); -} - -void -TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) -{ - addUserMessage(ty, body, RelatedInfo()); -} - -void -TimelineView::handleNewUserMessage(PendingMessage msg) -{ - pending_msgs_.enqueue(msg); - if (pending_msgs_.size() == 1 && pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); -} - -void -TimelineView::sendNextPendingMessage() -{ - if (pending_msgs_.size() == 0) - return; - - using namespace mtx::events; - - PendingMessage &m = pending_msgs_.head(); - - nhlog::ui()->debug("[{}] sending next queued message", m.txn_id); - - if (m.widget) - m.widget->markSent(); - - if (m.is_encrypted) { - nhlog::ui()->debug("[{}] sending encrypted event", m.txn_id); - prepareEncryptedMessage(std::move(m)); - return; - } - - switch (m.ty) { - case mtx::events::MessageType::Audio: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Image: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Video: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::File: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Text: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Emote: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - break; - } - default: - nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString()); - break; - } -} - -void -TimelineView::notifyForLastEvent() -{ - if (scroll_layout_->count() == 0) { - nhlog::ui()->error("notifyForLastEvent called with empty timeline"); - return; - } - - auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1); - - if (!lastItem) - return; - - auto *lastTimelineItem = qobject_cast(lastItem->widget()); - - if (lastTimelineItem) - emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); - else - nhlog::ui()->warn("cast to TimelineItem failed: {}", room_id_.toStdString()); -} - -void -TimelineView::notifyForLastEvent(const TimelineEvent &event) -{ - auto descInfo = utils::getMessageDescription(event, local_user_, room_id_); - - if (!descInfo.timestamp.isEmpty()) - emit updateLastTimelineMessage(room_id_, descInfo); -} - -bool -TimelineView::isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &local_userid) -{ - if (sender != local_userid) - return false; - - auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; }; - - return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) || - std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid); -} - -void -TimelineView::removePendingMessage(const std::string &txn_id) -{ - if (txn_id.empty()) - return; - - for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - int index = std::distance(pending_sent_msgs_.begin(), it); - pending_sent_msgs_.removeAt(index); - - if (pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); - - nhlog::ui()->debug("[{}] removed message with sync", txn_id); - } - } - for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - if (it->widget) { - it->widget->markReceived(it->is_encrypted); - - // TODO: update when a solution for encrypted messages is available. - if (!it->is_encrypted) - cache::client()->addPendingReceipt(room_id_, it->event_id); - } - - nhlog::ui()->debug("[{}] received sync before message response", txn_id); - return; - } - } -} - -void -TimelineView::handleFailedMessage(const std::string &txn_id) -{ - Q_UNUSED(txn_id); - // Note: We do this even if the message has already been echoed. - QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage())); -} - -void -TimelineView::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineView::readLastEvent() const -{ - if (!ChatPage::instance()->userSettings()->isReadReceiptsEnabled()) - return; - - const auto eventId = getLastEventId(); - - if (!eventId.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - eventId.toStdString(), - [this, eventId](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read event ({}, {})", - room_id_.toStdString(), - eventId.toStdString()); - } - }); -} - -QString -TimelineView::getLastEventId() const -{ - auto index = scroll_layout_->count(); - - // Search backwards for the first event that has a valid event id. - while (index > 0) { - --index; - - auto lastItem = scroll_layout_->itemAt(index); - auto *lastTimelineItem = qobject_cast(lastItem->widget()); - - if (lastTimelineItem && !lastTimelineItem->eventId().isEmpty()) - return lastTimelineItem->eventId(); - } - - return QString(""); -} - -void -TimelineView::showEvent(QShowEvent *event) -{ - if (!topMessages_.empty()) { - renderTopEvents(topMessages_); - topMessages_.clear(); - } - - if (!bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - bottomMessages_.clear(); - scrollDown(); - } - - toggleScrollDownButton(); - - readLastEvent(); - - QWidget::showEvent(event); -} - -void -TimelineView::hideEvent(QHideEvent *event) -{ - const auto handleHeight = scroll_area_->verticalScrollBar()->sizeHint().height(); - const auto widgetsNum = scroll_layout_->count(); - - // Remove widgets from the timeline to reduce the memory footprint. - if (handleHeight < MIN_SCROLLBAR_HANDLE && widgetsNum > MAX_RETAINED_WIDGETS) - clearTimeline(); - - QWidget::hideEvent(event); -} - -bool -TimelineView::event(QEvent *event) -{ - if (event->type() == QEvent::WindowActivate) - readLastEvent(); - - return QWidget::event(event); -} - -void -TimelineView::clearTimeline() -{ - // Delete all widgets. - QLayoutItem *item; - while ((item = scroll_layout_->takeAt(0)) != nullptr) { - delete item->widget(); - delete item; - } - - // The next call to /messages will be without a prev token. - prev_batch_token_.clear(); - eventIds_.clear(); - - // Clear queues with pending messages to be rendered. - bottomMessages_.clear(); - topMessages_.clear(); - - firstSender_.clear(); - lastSender_.clear(); -} - -void -TimelineView::toggleScrollDownButton() -{ - const int maxScroll = scroll_area_->verticalScrollBar()->maximum(); - const int currentScroll = scroll_area_->verticalScrollBar()->value(); - - if (maxScroll - currentScroll > SCROLL_BAR_GAP) { - scrollDownBtn_->show(); - scrollDownBtn_->raise(); - } else { - scrollDownBtn_->hide(); - } -} - -void -TimelineView::removeEvent(const QString &event_id) -{ - if (!eventIds_.contains(event_id)) { - nhlog::ui()->warn("cannot remove widget with unknown event_id: {}", - event_id.toStdString()); - return; - } - - auto removedItem = eventIds_[event_id]; - - // Find the next and the previous widgets in the timeline - auto prevWidget = relativeWidget(removedItem, -1); - auto nextWidget = relativeWidget(removedItem, 1); - - // See if they are timeline items - auto prevItem = qobject_cast(prevWidget); - auto nextItem = qobject_cast(nextWidget); - - // ... or a date separator - auto prevLabel = qobject_cast(prevWidget); - - // If it's a TimelineItem add an avatar. - if (prevItem) { - prevItem->addAvatar(); - } - - if (nextItem) { - nextItem->addAvatar(); - } else if (prevLabel) { - // If there's no chat message after this, and we have a label before us, delete the - // label. - prevLabel->deleteLater(); - } - - // If we deleted the last item in the timeline... - if (!nextItem && prevItem) - saveLastMessageInfo(prevItem->descriptionMessage().userid, - prevItem->descriptionMessage().datetime); - - // If we deleted the first item in the timeline... - if (!prevItem && nextItem) - saveFirstMessageInfo(nextItem->descriptionMessage().userid, - nextItem->descriptionMessage().datetime); - - // If we deleted the only item in the timeline... - if (!prevItem && !nextItem) { - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - lastSender_.clear(); - lastMsgTimestamp_ = QDateTime(); - } - - // Finally remove the event. - removedItem->deleteLater(); - eventIds_.remove(event_id); - - // Update the room list with a view of the last message after - // all events have been processed. - QTimer::singleShot(0, this, [this]() { notifyForLastEvent(); }); -} - -QWidget * -TimelineView::relativeWidget(QWidget *item, int dt) const -{ - int pos = scroll_layout_->indexOf(item); - - if (pos == -1) - return nullptr; - - pos = pos + dt; - - bool isOutOfBounds = (pos < 0 || pos > scroll_layout_->count() - 1); - - return isOutOfBounds ? nullptr : scroll_layout_->itemAt(pos)->widget(); -} - -TimelineEvent -TimelineView::findFirstViewableEvent(const std::vector &events) -{ - auto it = std::find_if(events.begin(), events.end(), [](const auto &event) { - return mtx::events::EventType::RoomMessage == utils::event_type(event); - }); - - return (it == std::end(events)) ? events.front() : *it; -} - -TimelineEvent -TimelineView::findLastViewableEvent(const std::vector &events) -{ - auto it = std::find_if(events.rbegin(), events.rend(), [](const auto &event) { - return (mtx::events::EventType::RoomMessage == utils::event_type(event)) || - (mtx::events::EventType::RoomEncrypted == utils::event_type(event)); - }); - - return (it == std::rend(events)) ? events.back() : *it; -} - -void -TimelineView::saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - updateLastSender(sender, direction); - - if (direction == TimelineDirection::Bottom) - lastMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); - else - firstMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); -} - -bool -TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second) const -{ - // Check if the dates are in a different day. - if (std::abs(first.daysTo(second)) != 0) - return true; - - const uint64_t diffInSeconds = std::abs(first.msecsTo(second)) / 1000; - constexpr uint64_t fifteenMins = 15 * 60; - - return diffInSeconds > fifteenMins; -} - -void -TimelineView::sendRoomMessageHandler(const std::string &txn_id, - 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); - return; - } - - emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string())); -} - -template<> -mtx::events::msg::Audio -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = m.mime.toStdString(); - audio.info.size = m.media_size; - audio.body = m.filename.toStdString(); - audio.url = m.body.toStdString(); - return audio; -} - -template<> -mtx::events::msg::Image -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::Image image; - image.info.mimetype = m.mime.toStdString(); - image.info.size = m.media_size; - image.body = m.filename.toStdString(); - image.url = m.body.toStdString(); - image.info.h = m.dimensions.height(); - image.info.w = m.dimensions.width(); - return image; -} - -template<> -mtx::events::msg::Video -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::Video video; - video.info.mimetype = m.mime.toStdString(); - video.info.size = m.media_size; - video.body = m.filename.toStdString(); - video.url = m.body.toStdString(); - return video; -} - -template<> -mtx::events::msg::Emote -toRoomMessage(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Emote emote; - emote.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) - emote.formatted_body = html.toStdString(); - - return emote; -} - -template<> -mtx::events::msg::File -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::File file; - file.info.mimetype = m.mime.toStdString(); - file.info.size = m.media_size; - file.body = m.filename.toStdString(); - file.url = m.body.toStdString(); - return file; -} - -template<> -mtx::events::msg::Text -toRoomMessage(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Text text; - - text.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) { - if (!m.related.quoted_body.isEmpty()) { - text.formatted_body = - utils::getFormattedQuoteBody(m.related, html).toStdString(); - } else { - text.formatted_body = html.toStdString(); - } - } - - if (!m.related.related_event.empty()) { - text.relates_to.in_reply_to.event_id = m.related.related_event; - } - - return text; -} - -void -TimelineView::prepareEncryptedMessage(const PendingMessage &msg) -{ - const auto room_id = room_id_.toStdString(); - - using namespace mtx::events; - using namespace mtx::identifiers; - - json content; - - // Serialize the message to the plaintext that will be encrypted. - switch (msg.ty) { - case MessageType::Audio: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Emote: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::File: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Image: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Text: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Video: { - content = json(toRoomMessage(msg)); - break; - } - default: - break; - } - - json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; - - try { - // Check if we have already an outbound megolm session then we can use. - if (cache::client()->outboundMegolmSessionExists(room_id)) { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client()->send_room_message( - room_id, - msg.txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - msg.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - return; - } - - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - auto outbound_session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(outbound_session.get()); - const auto session_key = mtx::crypto::session_key(outbound_session.get()); - - // TODO: needs to be moved in the lib. - auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, - {"room_id", room_id}, - {"session_id", session_id}, - {"session_key", session_key}}; - - // Saving the new megolm session. - // TODO: Maybe it's too early to save. - OutboundGroupSessionData session_data; - session_data.session_id = session_id; - session_data.session_key = session_key; - session_data.message_index = 0; // TODO Update me - cache::client()->saveOutboundMegolmSession( - room_id, session_data, std::move(outbound_session)); - - const auto members = cache::client()->roomMembers(room_id); - nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - - auto keeper = std::make_shared( - [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() { - try { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client() - ->send_room_message( - room_id, - txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save megolm outbound session: {}", e.what()); - } - }); - - mtx::requests::QueryKeys req; - for (const auto &member : members) - req.device_keys[member] = {}; - - http::client()->query_keys( - req, - [keeper = std::move(keeper), megolm_payload, 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. - return; - } - - for (const auto &user : res.device_keys) { - // Mapping from a device_id with valid identity keys to the - // generated room_key event used for sharing the megolm session. - std::map room_key_msgs; - std::map deviceKeys; - - room_key_msgs.clear(); - deviceKeys.clear(); - - for (const auto &dev : user.second) { - const auto user_id = UserId(dev.second.user_id); - const auto device_id = DeviceId(dev.second.device_id); - - const auto device_keys = dev.second.keys; - const auto curveKey = "curve25519:" + device_id.get(); - const auto edKey = "ed25519:" + device_id.get(); - - if ((device_keys.find(curveKey) == device_keys.end()) || - (device_keys.find(edKey) == device_keys.end())) { - nhlog::net()->debug( - "ignoring malformed keys for device {}", - device_id.get()); - continue; - } - - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - try { - if (!mtx::crypto::verify_identity_signature( - json(dev.second), device_id, user_id)) { - nhlog::crypto()->warn( - "failed to verify identity keys: {}", - json(dev.second).dump(2)); - continue; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn( - "failed to parse device key json: {}", - e.what()); - continue; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn( - "failed to verify device key json: {}", - e.what()); - continue; - } - - auto room_key = olm::client() - ->create_room_key_event( - user_id, pks.ed25519, megolm_payload) - .dump(); - - room_key_msgs.emplace(device_id, room_key); - deviceKeys.emplace(device_id, pks); - } - - std::vector valid_devices; - valid_devices.reserve(room_key_msgs.size()); - for (auto const &d : room_key_msgs) { - valid_devices.push_back(d.first); - - nhlog::net()->info("{}", d.first); - nhlog::net()->info(" curve25519 {}", - deviceKeys.at(d.first).curve25519); - nhlog::net()->info(" ed25519 {}", - deviceKeys.at(d.first).ed25519); - } - - nhlog::net()->info( - "sending claim request for user {} with {} devices", - user.first, - valid_devices.size()); - - http::client()->claim_keys( - user.first, - valid_devices, - std::bind(&TimelineView::handleClaimedKeys, - this, - keeper, - room_key_msgs, - deviceKeys, - user.first, - std::placeholders::_1, - std::placeholders::_2)); - - // TODO: Wait before sending the next batch of requests. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - // TODO: Let the user know about the errors. - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } -} - -void -TimelineView::handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_keys, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) -{ - if (err) { - nhlog::net()->warn("claim keys error: {} {} {}", - err->matrix_error.error, - err->parse_error, - static_cast(err->status_code)); - return; - } - - nhlog::net()->debug("claimed keys for {}", user_id); - - if (res.one_time_keys.size() == 0) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - auto retrieved_devices = res.one_time_keys.at(user_id); - - // Payload with all the to_device message to be sent. - json body; - body["messages"][user_id] = json::object(); - - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); - - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); - - if (pks.find(device_id) == pks.end()) { - nhlog::net()->critical("couldn't find public key for device: {}", - device_id); - continue; - } - - auto id_key = pks.at(device_id).curve25519; - auto s = olm::client()->create_outbound_session(id_key, otk); - - if (room_keys.find(device_id) == room_keys.end()) { - nhlog::net()->critical("couldn't find m.room_key for device: {}", - device_id); - continue; - } - - auto device_msg = olm::client()->create_olm_encrypted_content( - s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); - - try { - cache::client()->saveOlmSession(id_key, std::move(s)); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to pickle outbound olm session: {}", - e.what()); - } - - body["messages"][user_id][device_id] = device_msg; - } - - nhlog::net()->info("send_to_device: {}", user_id); - - http::client()->send_to_device( - "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - (void)keeper; - }); -} diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h deleted file mode 100644 index 35796efd..00000000 --- a/src/timeline/TimelineView.h +++ /dev/null @@ -1,449 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "../Utils.h" -#include "MatrixClient.h" -#include "timeline/TimelineItem.h" - -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. - utils::TimelineEvent event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; -}; - -class FloatingButton; -struct DescInfo; - -// Contains info about a message shown in the history view -// but not yet confirmed by the homeserver through sync. -struct PendingMessage -{ - mtx::events::MessageType ty; - std::string txn_id; - RelatedInfo related; - QString body; - QString filename; - QString mime; - uint64_t media_size; - QString event_id; - TimelineItem *widget; - QSize dimensions; - bool is_encrypted = false; -}; - -template -MessageT -toRoomMessage(const PendingMessage &) = delete; - -template<> -mtx::events::msg::Audio -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::Emote -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::File -toRoomMessage(const PendingMessage &); - -template<> -mtx::events::msg::Image -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::Text -toRoomMessage(const PendingMessage &); - -template<> -mtx::events::msg::Video -toRoomMessage(const PendingMessage &m); - -// In which place new TimelineItems should be inserted. -enum class TimelineDirection -{ - Top, - Bottom, -}; - -class TimelineView : public QWidget -{ - Q_OBJECT - -public: - TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent = 0); - TimelineView(const QString &room_id, QWidget *parent = 0); - - // Add new events at the end of the timeline. - void addEvents(const mtx::responses::Timeline &timeline); - void addUserMessage(mtx::events::MessageType ty, - const QString &body, - const RelatedInfo &related); - void addUserMessage(mtx::events::MessageType ty, const QString &msg); - - template - void addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions = QSize()); - void updatePendingMessage(const std::string &txn_id, const QString &event_id); - void scrollDown(); - - //! Remove an item from the timeline with the given Event ID. - void removeEvent(const QString &event_id); - void setPrevBatchToken(const QString &token) { prev_batch_token_ = token; } - -public slots: - void sliderRangeChanged(int min, int max); - void sliderMoved(int position); - void fetchHistory(); - - // Add old events at the top of the timeline. - void addBackwardsEvents(const mtx::responses::Messages &msgs); - - // Whether or not the initial batch has been loaded. - bool hasLoaded() { return scroll_layout_->count() > 0 || isTimelineFinished; } - - void handleFailedMessage(const std::string &txn_id); - -private slots: - void sendNextPendingMessage(); - -signals: - void updateLastTimelineMessage(const QString &user, const DescInfo &info); - void messagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(const std::string &txn_id); - void messageSent(const std::string &txn_id, const QString &event_id); - void markReadEvents(const std::vector &event_ids); - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - bool event(QEvent *event) override; - -private: - using TimelineEvent = mtx::events::collections::TimelineEvents; - - //! Mark our own widgets as read if they have more than one receipt. - void displayReadReceipts(std::vector events); - //! Determine if the start of the timeline is reached from the response of /messages. - bool isStartOfTimeline(const mtx::responses::Messages &msgs); - - QWidget *relativeWidget(QWidget *item, int dt) const; - - DecryptionResult parseEncryptedEvent( - const mtx::events::EncryptedEvent &e); - - 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); - - //! Callback for all message sending. - void sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err); - void prepareEncryptedMessage(const PendingMessage &msg); - - //! Call the /messages endpoint to fill the timeline. - void getMessages(); - //! HACK: Fixing layout flickering when adding to the bottom - //! of the timeline. - void pushTimelineItem(QWidget *item, TimelineDirection dir) - { - setUpdatesEnabled(false); - item->hide(); - - if (dir == TimelineDirection::Top) - scroll_layout_->insertWidget(0, item); - else - scroll_layout_->addWidget(item); - - QTimer::singleShot(0, this, [item, this]() { - item->show(); - item->adjustSize(); - setUpdatesEnabled(true); - }); - } - - //! Decides whether or not to show or hide the scroll down button. - void toggleScrollDownButton(); - void init(); - void addTimelineItem(QWidget *item, - TimelineDirection direction = TimelineDirection::Bottom); - void updateLastSender(const QString &user_id, TimelineDirection direction); - void notifyForLastEvent(); - void notifyForLastEvent(const TimelineEvent &event); - //! Keep track of the sender and the timestamp of the current message. - void saveLastMessageInfo(const QString &sender, const QDateTime &datetime) - { - lastSender_ = sender; - lastMsgTimestamp_ = datetime; - } - void saveFirstMessageInfo(const QString &sender, const QDateTime &datetime) - { - firstSender_ = sender; - firstMsgTimestamp_ = datetime; - } - //! Keep track of the sender and the timestamp of the current message. - void saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction); - - TimelineEvent findFirstViewableEvent(const std::vector &events); - TimelineEvent findLastViewableEvent(const std::vector &events); - - //! Mark the last event as read. - void readLastEvent() const; - //! Whether or not the scrollbar is visible (non-zero height). - bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; } - //! Retrieve the event id of the last item. - QString getLastEventId() const; - - template - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // TODO: Remove this eventually. - template - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // For events with custom display widgets. - template - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // For events without custom display widgets. - // TODO: All events should have custom widgets. - template - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // Used to determine whether or not we should prefix a message with the - // sender's name. - bool isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction); - - bool isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &userid); - void removePendingMessage(const std::string &txn_id); - - bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); } - - void handleNewUserMessage(PendingMessage msg); - bool isDateDifference(const QDateTime &first, - const QDateTime &second = QDateTime::currentDateTime()) const; - - // Return nullptr if the event couldn't be parsed. - QWidget *parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction); - - //! Store the event id associated with the given widget. - void saveEventId(QWidget *widget); - //! Remove all widgets from the timeline layout. - void clearTimeline(); - - QVBoxLayout *top_layout_; - QVBoxLayout *scroll_layout_; - - QScrollArea *scroll_area_; - QWidget *scroll_widget_; - - QString firstSender_; - QDateTime firstMsgTimestamp_; - QString lastSender_; - QDateTime lastMsgTimestamp_; - - QString room_id_; - QString prev_batch_token_; - QString local_user_; - - bool isPaginationInProgress_ = false; - - // Keeps track whether or not the user has visited the view. - bool isInitialized = false; - bool isTimelineFinished = false; - bool isInitialSync = true; - - const int SCROLL_BAR_GAP = 200; - - QTimer *paginationTimer_; - - int scroll_height_ = 0; - int previous_max_height_ = 0; - - int oldPosition_; - int oldHeight_; - - FloatingButton *scrollDownBtn_; - - TimelineDirection lastMessageDirection_; - - //! Messages received by sync not added to the timeline. - std::vector bottomMessages_; - //! Messages received by /messages not added to the timeline. - std::vector topMessages_; - - //! Render the given timeline events to the bottom of the timeline. - void renderBottomEvents(const std::vector &events); - //! Render the given timeline events to the top of the timeline. - void renderTopEvents(const std::vector &events); - - // The events currently rendered. Used for duplicate detection. - QMap eventIds_; - QQueue pending_msgs_; - QList pending_sent_msgs_; -}; - -template -void -TimelineView::addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - auto trimmed = QFileInfo{filename}.fileName(); // Trim file path. - - auto widget = new Widget(url, trimmed, size, this); - - TimelineItem *view_item = - new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_); - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - // Keep track of the sender and the timestamp of the current message. - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - - PendingMessage message; - message.ty = MsgType; - message.txn_id = http::client()->generate_txn_id(); - message.body = url; - message.filename = trimmed; - message.mime = mime; - message.media_size = size; - message.widget = view_item; - message.dimensions = dimensions; - - handleNewUserMessage(message); -} - -template -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_); - return item; -} - -template -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - auto eventWidget = new Widget(event); - auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_); - - return item; -} - -template -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} - -template -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 86505481..d733ad90 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -1,94 +1,339 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ +#include "TimelineViewManager.h" -#include +#include +#include +#include +#include +#include +#include -#include -#include -#include - -#include "Cache.h" +#include "ChatPage.h" +#include "ColorImageProvider.h" +#include "DelegateChooser.h" #include "Logging.h" -#include "Utils.h" -#include "timeline/TimelineView.h" -#include "timeline/TimelineViewManager.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" +#include "MxcImageProvider.h" +#include "UserSettingsPage.h" +#include "dialogs/ImageOverlay.h" + +void +TimelineViewManager::updateColorPalette() +{ + UserSettings settings; + if (settings.theme() == "light") { + QPalette lightActive(/*windowText*/ QColor("#333"), + /*button*/ QColor("#333"), + /*light*/ QColor(), + /*dark*/ QColor(220, 220, 220, 120), + /*mid*/ QColor(), + /*text*/ QColor("#333"), + /*bright_text*/ QColor(), + /*base*/ QColor("white"), + /*window*/ QColor("white")); + view->rootContext()->setContextProperty("currentActivePalette", lightActive); + view->rootContext()->setContextProperty("currentInactivePalette", lightActive); + } else if (settings.theme() == "dark") { + QPalette darkActive(/*windowText*/ QColor("#caccd1"), + /*button*/ QColor("#caccd1"), + /*light*/ QColor(), + /*dark*/ QColor(45, 49, 57, 120), + /*mid*/ QColor(), + /*text*/ QColor("#caccd1"), + /*bright_text*/ QColor(), + /*base*/ QColor("#202228"), + /*window*/ QColor("#202228")); + darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9")); + view->rootContext()->setContextProperty("currentActivePalette", darkActive); + view->rootContext()->setContextProperty("currentInactivePalette", darkActive); + } else { + view->rootContext()->setContextProperty("currentActivePalette", QPalette()); + view->rootContext()->setContextProperty("currentInactivePalette", nullptr); + } +} TimelineViewManager::TimelineViewManager(QWidget *parent) - : QStackedWidget(parent) -{} + : imgProvider(new MxcImageProvider()) + , colorImgProvider(new ColorImageProvider()) +{ + qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, + "com.github.nheko", + 1, + 0, + "MtxEvent", + "Can't instantiate enum!"); + qmlRegisterType("com.github.nheko", 1, 0, "DelegateChoice"); + qmlRegisterType("com.github.nheko", 1, 0, "DelegateChooser"); + +#ifdef USE_QUICK_VIEW + view = new QQuickView(); + container = QWidget::createWindowContainer(view, parent); +#else + view = new QQuickWidget(parent); + container = view; + view->setResizeMode(QQuickWidget::SizeRootObjectToView); + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { + nhlog::ui()->debug("Status changed to {}", status); + }); +#endif + container->setMinimumSize(200, 200); + view->rootContext()->setContextProperty("timelineManager", this); + updateColorPalette(); + view->engine()->addImageProvider("MxcImage", imgProvider); + view->engine()->addImageProvider("colorimage", colorImgProvider); + view->setSource(QUrl("qrc:///qml/TimelineView.qml")); + + connect(dynamic_cast(parent), + &ChatPage::themeChanged, + this, + &TimelineViewManager::updateColorPalette); +} + +void +TimelineViewManager::sync(const mtx::responses::Rooms &rooms) +{ + for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { + // 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); + } +} + +void +TimelineViewManager::addRoom(const QString &room_id) +{ + if (!models.contains(room_id)) + models.insert(room_id, + QSharedPointer(new TimelineModel(this, room_id))); +} + +void +TimelineViewManager::setHistoryView(const QString &room_id) +{ + nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); + + auto room = models.find(room_id); + if (room != models.end()) { + timeline_ = room.value().data(); + emit activeTimelineChanged(timeline_); + nhlog::ui()->info("Activated room {}", room_id.toStdString()); + } +} + +void +TimelineViewManager::openImageOverlay(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) 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(), 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; + } + + 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()); + }); +} void TimelineViewManager::updateReadReceipts(const QString &room_id, const std::vector &event_ids) { - if (timelineViewExists(room_id)) { - auto view = views_[room_id]; - if (view) - emit view->markReadEvents(event_ids); + auto room = models.find(room_id); + if (room != models.end()) { + room.value()->markEventsAsRead(event_ids); } } void -TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) +TimelineViewManager::initWithMessages(const std::map &msgs) { - auto view = views_[room_id]; + for (const auto &e : msgs) { + addRoom(e.first); - if (view) - view->removeEvent(event_id); + models.value(e.first)->addEvents(e.second); + } } void TimelineViewManager::queueTextMessage(const QString &msg) { - if (active_room_.isEmpty()) - return; + mtx::events::msg::Text text = {}; + text.body = msg.trimmed().toStdString(); + text.format = "org.matrix.custom.html"; + text.formatted_body = utils::markdownToHtml(msg).toStdString(); - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Text, msg); -} - -void -TimelineViewManager::queueEmoteMessage(const QString &msg) -{ - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Emote, msg); + if (timeline_) + timeline_->sendMessage(text); } void TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) { - if (active_room_.isEmpty()) - return; + mtx::events::msg::Text text = {}; - auto room_id = active_room_; - auto view = views_[room_id]; + QString body; + bool firstLine = true; + for (const auto &line : related.quoted_body.split("\n")) { + if (firstLine) { + firstLine = false; + body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); + } else { + body = QString("%1\n> %2\n").arg(body).arg(line); + } + } - view->addUserMessage(mtx::events::MessageType::Text, reply, related); + text.body = QString("%1\n%2").arg(body).arg(reply).toStdString(); + text.format = "org.matrix.custom.html"; + text.formatted_body = + utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString(); + text.relates_to.in_reply_to.event_id = related.related_event; + + if (timeline_) + timeline_->sendMessage(text); +} + +void +TimelineViewManager::queueEmoteMessage(const QString &msg) +{ + auto html = utils::markdownToHtml(msg); + + mtx::events::msg::Emote emote; + emote.body = msg.trimmed().toStdString(); + + if (html != msg.trimmed().toHtmlEscaped()) + emote.formatted_body = html.toStdString(); + + if (timeline_) + timeline_->sendMessage(emote); } void @@ -96,18 +341,17 @@ TimelineViewManager::queueImageMessage(const QString &roomid, const QString &filename, const QString &url, const QString &mime, - uint64_t size, + uint64_t dsize, const QSize &dimensions) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("Cannot send m.image message to a non-managed view"); - return; - } - - auto view = views_[roomid]; - - view->addUserMessage( - url, filename, mime, size, dimensions); + mtx::events::msg::Image image; + image.info.mimetype = mime.toStdString(); + image.info.size = dsize; + image.body = filename.toStdString(); + image.url = url.toStdString(); + image.info.h = dimensions.height(); + image.info.w = dimensions.width(); + models.value(roomid)->sendMessage(image); } void @@ -115,16 +359,14 @@ TimelineViewManager::queueFileMessage(const QString &roomid, const QString &filename, const QString &url, const QString &mime, - uint64_t size) + uint64_t dsize) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.file message to a non-managed view"); - return; - } - - auto view = views_[roomid]; - - view->addUserMessage(url, filename, mime, size); + mtx::events::msg::File file; + file.info.mimetype = mime.toStdString(); + file.info.size = dsize; + file.body = filename.toStdString(); + file.url = url.toStdString(); + models.value(roomid)->sendMessage(file); } void @@ -132,16 +374,14 @@ TimelineViewManager::queueAudioMessage(const QString &roomid, const QString &filename, const QString &url, const QString &mime, - uint64_t size) + uint64_t dsize) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.audio message to a non-managed view"); - return; - } - - auto view = views_[roomid]; - - view->addUserMessage(url, filename, mime, size); + mtx::events::msg::Audio audio; + audio.info.mimetype = mime.toStdString(); + audio.info.size = dsize; + audio.body = filename.toStdString(); + audio.url = url.toStdString(); + models.value(roomid)->sendMessage(audio); } void @@ -149,192 +389,12 @@ TimelineViewManager::queueVideoMessage(const QString &roomid, const QString &filename, const QString &url, const QString &mime, - uint64_t size) + uint64_t dsize) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.video message to a non-managed view"); - return; - } - - auto view = views_[roomid]; - - view->addUserMessage(url, filename, mime, size); -} - -void -TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) -{ - for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { - addRoom(it->second, QString::fromStdString(it->first)); - } - - sync(rooms); -} - -void -TimelineViewManager::initWithMessages(const std::map &msgs) -{ - for (auto it = msgs.cbegin(); it != msgs.cend(); ++it) { - if (timelineViewExists(it->first)) - return; - - // Create a history view with the room events. - TimelineView *view = new TimelineView(it->second, it->first); - views_.emplace(it->first, QSharedPointer(view)); - - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(view); - } -} - -void -TimelineViewManager::initialize(const std::vector &rooms) -{ - for (const auto &roomid : rooms) - addRoom(QString::fromStdString(roomid)); -} - -void -TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id) -{ - if (timelineViewExists(room_id)) - return; - - // Create a history view with the room events. - TimelineView *view = new TimelineView(room.timeline, room_id); - views_.emplace(room_id, QSharedPointer(view)); - - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(view); -} - -void -TimelineViewManager::addRoom(const QString &room_id) -{ - if (timelineViewExists(room_id)) - return; - - // Create a history view without any events. - TimelineView *view = new TimelineView(room_id); - views_.emplace(room_id, QSharedPointer(view)); - - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(view); -} - -void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) -{ - for (const auto &room : rooms.join) { - auto roomid = QString::fromStdString(room.first); - - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("ignoring event from unknown room: {}", - roomid.toStdString()); - continue; - } - - auto view = views_.at(roomid); - - view->addEvents(room.second.timeline); - } -} - -void -TimelineViewManager::setHistoryView(const QString &room_id) -{ - if (!timelineViewExists(room_id)) { - nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}", - room_id.toStdString()); - return; - } - - active_room_ = room_id; - auto view = views_.at(room_id); - - setCurrentWidget(view.data()); - - view->fetchHistory(); - view->scrollDown(); -} - -QString -TimelineViewManager::chooseRandomColor() -{ - std::random_device random_device; - std::mt19937 engine{random_device()}; - std::uniform_real_distribution dist(0, 1); - - float hue = dist(engine); - float saturation = 0.9; - float value = 0.7; - - int hue_i = hue * 6; - - float f = hue * 6 - hue_i; - - float p = value * (1 - saturation); - float q = value * (1 - f * saturation); - float t = value * (1 - (1 - f) * saturation); - - float r = 0; - float g = 0; - float b = 0; - - if (hue_i == 0) { - r = value; - g = t; - b = p; - } else if (hue_i == 1) { - r = q; - g = value; - b = p; - } else if (hue_i == 2) { - r = p; - g = value; - b = t; - } else if (hue_i == 3) { - r = p; - g = q; - b = value; - } else if (hue_i == 4) { - r = t; - g = p; - b = value; - } else if (hue_i == 5) { - r = value; - g = p; - b = q; - } - - int ri = r * 256; - int gi = g * 256; - int bi = b * 256; - - QColor color(ri, gi, bi); - - return color.name(); -} - -bool -TimelineViewManager::hasLoaded() const -{ - return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) { - return view.second->hasLoaded(); - }); + mtx::events::msg::Video video; + video.info.mimetype = mime.toStdString(); + video.info.size = dsize; + video.body = filename.toStdString(); + video.url = url.toStdString(); + models.value(roomid)->sendMessage(video); } diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index b52136d9..691c8ddb 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -1,69 +1,80 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - #pragma once +#include +#include #include -#include +#include -#include +#include +#include "Cache.h" +#include "Logging.h" +#include "TimelineModel.h" #include "Utils.h" -class QFile; +// temporary for stubs +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" -class RoomInfoListItem; -class TimelineView; -struct DescInfo; -struct SavedMessages; +class MxcImageProvider; +class ColorImageProvider; -class TimelineViewManager : public QStackedWidget +class TimelineViewManager : public QObject { Q_OBJECT + Q_PROPERTY( + TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) + public: - TimelineViewManager(QWidget *parent); - - // Initialize with timeline events. - void initialize(const mtx::responses::Rooms &rooms); - // Empty initialization. - void initialize(const std::vector &rooms); - - void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id); - void addRoom(const QString &room_id); + TimelineViewManager(QWidget *parent = 0); + QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); - void clearAll() { views_.clear(); } + void addRoom(const QString &room_id); - // Check if all the timelines have been loaded. - bool hasLoaded() const; + void clearAll() { models.clear(); } - static QString chooseRandomColor(); + Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } + 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); + } signals: void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(const QString &user, const DescInfo &info); + void updateRoomsLastMessage(QString roomid, const DescInfo &info); + void activeTimelineChanged(TimelineModel *timeline); + void mediaCached(QString mxcUrl, QString cacheUrl); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); - void removeTimelineEvent(const QString &room_id, const QString &event_id); void initWithMessages(const std::map &msgs); void setHistoryView(const QString &room_id); + void updateColorPalette(); + void queueTextMessage(const QString &msg); void queueReplyMessage(const QString &reply, const RelatedInfo &related); void queueEmoteMessage(const QString &msg); @@ -90,9 +101,17 @@ public slots: uint64_t dsize); private: - //! Check if the given room id is managed by a TimelineView. - bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } +#ifdef USE_QUICK_VIEW + QQuickView *view; +#else + QQuickWidget *view; +#endif + QWidget *container; + TimelineModel *timeline_ = nullptr; + MxcImageProvider *imgProvider; + ColorImageProvider *colorImgProvider; - QString active_room_; - std::map> views_; + QHash> models; }; + +#pragma GCC diagnostic pop diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp deleted file mode 100644 index 5d6431ee..00000000 --- a/src/timeline/widgets/AudioItem.cpp +++ /dev/null @@ -1,236 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/AudioItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int ActionIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -AudioItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - playIcon_.addFile(":/icons/icons/ui/play-sign.png"); - pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png"); - - player_ = new QMediaPlayer; - player_->setMedia(QUrl(url_)); - player_->setVolume(100); - player_->setNotifyInterval(1000); - - connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) { - if (state == QMediaPlayer::StoppedState) { - state_ = AudioState::Play; - player_->setMedia(QUrl(url_)); - update(); - } - }); - - setFixedHeight(Height); -} - -AudioItem::AudioItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , url_{QUrl(QString::fromStdString(event.content.url))} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -AudioItem::AudioItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -QSize -AudioItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -AudioItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - if (state_ == AudioState::Play) { - state_ = AudioState::Pause; - player_->play(); - } else { - state_ = AudioState::Play; - player_->pause(); - } - - update(); - } else { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &AudioItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->info("failed to retrieve m.audio content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } -} - -void -AudioItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("error while saving file: {}", e.what()); - } -} - -void -AudioItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); -#else - const int computedWidth = - std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, - (double)MaxWidth); -#endif - resize(computedWidth, Height); - - event->accept(); -} - -void -AudioItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - QIcon icon_; - if (state_ == AudioState::Play) - icon_ = playIcon_; - else - icon_ = pauseIcon_; - - icon_.paint(&painter, - QRect(IconXCenter - ActionIconRadius / 2, - IconYCenter - ActionIconRadius / 2, - ActionIconRadius, - ActionIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/AudioItem.h b/src/timeline/widgets/AudioItem.h deleted file mode 100644 index c32b7731..00000000 --- a/src/timeline/widgets/AudioItem.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include - -class AudioItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - - Q_PROPERTY(QColor durationBackgroundColor WRITE setDurationBackgroundColor READ - durationBackgroundColor) - Q_PROPERTY(QColor durationForegroundColor WRITE setDurationForegroundColor READ - durationForegroundColor) - -public: - AudioItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - AudioItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - void setDurationBackgroundColor(const QColor &color) { durationBgColor_ = color; } - void setDurationForegroundColor(const QColor &color) { durationFgColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - - QColor durationBackgroundColor() const { return durationBgColor_; } - QColor durationForegroundColor() const { return durationFgColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void init(); - - enum class AudioState - { - Play, - Pause, - }; - - AudioState state_ = AudioState::Play; - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent event_; - - QMediaPlayer *player_; - - QIcon playIcon_; - QIcon pauseIcon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); - - QColor durationBgColor_ = QColor("black"); - QColor durationFgColor_ = QColor("blue"); -}; diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp deleted file mode 100644 index 1a555d1c..00000000 --- a/src/timeline/widgets/FileItem.cpp +++ /dev/null @@ -1,221 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/FileItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int DownloadIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -FileItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png"); - - setFixedHeight(Height); -} - -FileItem::FileItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -FileItem::FileItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -void -FileItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString()); -} - -QSize -FileItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -FileItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &FileItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->warn("failed to retrieve m.file content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } else { - openUrl(); - } -} - -void -FileItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -FileItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); -#else - const int computedWidth = - std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, - (double)MaxWidth); -#endif - resize(computedWidth, Height); - - event->accept(); -} - -void -FileItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - icon_.paint(&painter, - QRect(IconXCenter - DownloadIconRadius / 2, - IconYCenter - DownloadIconRadius / 2, - DownloadIconRadius, - DownloadIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/FileItem.h b/src/timeline/widgets/FileItem.h deleted file mode 100644 index d63cce88..00000000 --- a/src/timeline/widgets/FileItem.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include - -class FileItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - FileItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - FileItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void openUrl(); - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent event_; - - QIcon icon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); -}; diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp deleted file mode 100644 index 26c569d7..00000000 --- a/src/timeline/widgets/ImageItem.cpp +++ /dev/null @@ -1,267 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Config.h" -#include "ImageItem.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "dialogs/ImageOverlay.h" - -void -ImageItem::downloadMedia(const QUrl &url) -{ - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::imageDownloaded, this, &ImageItem::setImage); - - http::client()->download(url.toString().toStdString(), - [proxy = std::move(proxy), 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.toString().toStdString(), - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - QPixmap img; - img.loadFromData(QByteArray(data.data(), data.size())); - - emit proxy->imageDownloaded(img); - }); -} - -void -ImageItem::saveImage(const QString &filename, const QByteArray &data) -{ - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -ImageItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - downloadMedia(url_); -} - -ImageItem::ImageItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , event_{event} -{ - url_ = QString::fromStdString(event.content.url); - text_ = QString::fromStdString(event.content.body); - - init(); -} - -ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - Q_UNUSED(size); - init(); -} - -void -ImageItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString()); -} - -QSize -ImageItem::sizeHint() const -{ - if (image_.isNull()) - return QSize(max_width_, bottom_height_); - - return QSize(width_, height_); -} - -void -ImageItem::setImage(const QPixmap &image) -{ - image_ = image; - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); - update(); -} - -void -ImageItem::mousePressEvent(QMouseEvent *event) -{ - if (!isInteractive_) { - event->accept(); - return; - } - - if (event->button() != Qt::LeftButton) - return; - - if (image_.isNull()) { - openUrl(); - return; - } - - if (textRegion_.contains(event->pos())) { - openUrl(); - } else { - auto imgDialog = new dialogs::ImageOverlay(image_); - imgDialog->show(); - connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs); - } -} - -void -ImageItem::resizeEvent(QResizeEvent *event) -{ - if (!image_) - return QWidget::resizeEvent(event); - - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); -} - -void -ImageItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - - QFontMetrics metrics(font); - const int fontHeight = metrics.height() + metrics.ascent(); - - if (image_.isNull()) { - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - setFixedSize(metrics.width(elidedText), fontHeight); -#else - setFixedSize(metrics.horizontalAdvance(elidedText), fontHeight); -#endif - painter.setFont(font); - painter.setPen(QPen(QColor(66, 133, 244))); - painter.drawText(QPoint(0, fontHeight / 2), elidedText); - - return; - } - - imageRegion_ = QRectF(0, 0, width_, height_); - - QPainterPath path; - path.addRoundedRect(imageRegion_, 5, 5); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, scaled_image_); - painter.drawPath(path); - - // Bottom text section - if (isInteractive_ && underMouse()) { - const int textBoxHeight = fontHeight / 2 + 6; - - textRegion_ = QRectF(0, height_ - textBoxHeight, width_, textBoxHeight); - - QPainterPath textPath; - textPath.addRoundedRect(textRegion_, 0, 0); - - painter.fillPath(textPath, QColor(40, 40, 40, 140)); - - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10); - - font.setWeight(QFont::Medium); - painter.setFont(font); - painter.setPen(QPen(QColor(Qt::white))); - - textRegion_.adjust(5, 0, 5, 0); - painter.drawText(textRegion_, Qt::AlignVCenter, elidedText); - } -} - -void -ImageItem::saveAs() -{ - auto filename = QFileDialog::getSaveFileName(this, tr("Save image"), text_); - - if (filename.isEmpty()) - return; - - const auto url = url_.toString().toStdString(); - - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::imageSaved, this, &ImageItem::saveImage); - - http::client()->download( - url, - [proxy = std::move(proxy), 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; - } - - emit proxy->imageSaved(filename, QByteArray(data.data(), data.size())); - }); -} diff --git a/src/timeline/widgets/ImageItem.h b/src/timeline/widgets/ImageItem.h deleted file mode 100644 index 65bd962d..00000000 --- a/src/timeline/widgets/ImageItem.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include - -#include - -namespace dialogs { -class ImageOverlay; -} - -class ImageItem : public QWidget -{ - Q_OBJECT -public: - ImageItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - ImageItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - -public slots: - //! Show a save as dialog for the image. - void saveAs(); - void setImage(const QPixmap &image); - void saveImage(const QString &filename, const QByteArray &data); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - - //! Whether the user can interact with the displayed image. - bool isInteractive_ = true; - -private: - void init(); - void openUrl(); - void downloadMedia(const QUrl &url); - - int max_width_ = 500; - int max_height_ = 300; - - int width_; - int height_; - - QPixmap scaled_image_; - QPixmap image_; - - QUrl url_; - QString text_; - - int bottom_height_ = 30; - - QRectF textRegion_; - QRectF imageRegion_; - - mtx::events::RoomEvent event_; -}; - -class StickerItem : public ImageItem -{ - Q_OBJECT - -public: - StickerItem(const mtx::events::Sticker &event, QWidget *parent = nullptr) - : ImageItem{QString::fromStdString(event.content.url), - QString::fromStdString(event.content.body), - event.content.info.size, - parent} - , event_{event} - { - isInteractive_ = false; - setCursor(Qt::ArrowCursor); - setMouseTracking(false); - setAttribute(Qt::WA_Hover, false); - } - -private: - mtx::events::Sticker event_; -}; diff --git a/src/timeline/widgets/VideoItem.cpp b/src/timeline/widgets/VideoItem.cpp deleted file mode 100644 index 4b5dc022..00000000 --- a/src/timeline/widgets/VideoItem.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include - -#include "Config.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "timeline/widgets/VideoItem.h" - -void -VideoItem::init() -{ - url_ = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); -} - -VideoItem::VideoItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); - - auto layout = new QVBoxLayout(this); - layout->setMargin(0); - layout->setSpacing(0); - - QString link = QString("%2").arg(url_.toString()).arg(text_); - - label_ = new QLabel(link, this); - label_->setMargin(0); - label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - label_->setOpenExternalLinks(true); - - layout->addWidget(label_); -} - -VideoItem::VideoItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} diff --git a/src/timeline/widgets/VideoItem.h b/src/timeline/widgets/VideoItem.h deleted file mode 100644 index 26fa1c35..00000000 --- a/src/timeline/widgets/VideoItem.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include - -class VideoItem : public QWidget -{ - Q_OBJECT - -public: - VideoItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - VideoItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - -private: - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - - QLabel *label_; - - mtx::events::RoomEvent event_; -}; diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp deleted file mode 100644 index d733ad90..00000000 --- a/src/timeline2/TimelineViewManager.cpp +++ /dev/null @@ -1,400 +0,0 @@ -#include "TimelineViewManager.h" - -#include -#include -#include -#include -#include -#include - -#include "ChatPage.h" -#include "ColorImageProvider.h" -#include "DelegateChooser.h" -#include "Logging.h" -#include "MxcImageProvider.h" -#include "UserSettingsPage.h" -#include "dialogs/ImageOverlay.h" - -void -TimelineViewManager::updateColorPalette() -{ - UserSettings settings; - if (settings.theme() == "light") { - QPalette lightActive(/*windowText*/ QColor("#333"), - /*button*/ QColor("#333"), - /*light*/ QColor(), - /*dark*/ QColor(220, 220, 220, 120), - /*mid*/ QColor(), - /*text*/ QColor("#333"), - /*bright_text*/ QColor(), - /*base*/ QColor("white"), - /*window*/ QColor("white")); - view->rootContext()->setContextProperty("currentActivePalette", lightActive); - view->rootContext()->setContextProperty("currentInactivePalette", lightActive); - } else if (settings.theme() == "dark") { - QPalette darkActive(/*windowText*/ QColor("#caccd1"), - /*button*/ QColor("#caccd1"), - /*light*/ QColor(), - /*dark*/ QColor(45, 49, 57, 120), - /*mid*/ QColor(), - /*text*/ QColor("#caccd1"), - /*bright_text*/ QColor(), - /*base*/ QColor("#202228"), - /*window*/ QColor("#202228")); - darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9")); - view->rootContext()->setContextProperty("currentActivePalette", darkActive); - view->rootContext()->setContextProperty("currentInactivePalette", darkActive); - } else { - view->rootContext()->setContextProperty("currentActivePalette", QPalette()); - view->rootContext()->setContextProperty("currentInactivePalette", nullptr); - } -} - -TimelineViewManager::TimelineViewManager(QWidget *parent) - : imgProvider(new MxcImageProvider()) - , colorImgProvider(new ColorImageProvider()) -{ - qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, - "com.github.nheko", - 1, - 0, - "MtxEvent", - "Can't instantiate enum!"); - qmlRegisterType("com.github.nheko", 1, 0, "DelegateChoice"); - qmlRegisterType("com.github.nheko", 1, 0, "DelegateChooser"); - -#ifdef USE_QUICK_VIEW - view = new QQuickView(); - container = QWidget::createWindowContainer(view, parent); -#else - view = new QQuickWidget(parent); - container = view; - view->setResizeMode(QQuickWidget::SizeRootObjectToView); - container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - - connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { - nhlog::ui()->debug("Status changed to {}", status); - }); -#endif - container->setMinimumSize(200, 200); - view->rootContext()->setContextProperty("timelineManager", this); - updateColorPalette(); - view->engine()->addImageProvider("MxcImage", imgProvider); - view->engine()->addImageProvider("colorimage", colorImgProvider); - view->setSource(QUrl("qrc:///qml/TimelineView.qml")); - - connect(dynamic_cast(parent), - &ChatPage::themeChanged, - this, - &TimelineViewManager::updateColorPalette); -} - -void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) -{ - for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { - // 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); - } -} - -void -TimelineViewManager::addRoom(const QString &room_id) -{ - if (!models.contains(room_id)) - models.insert(room_id, - QSharedPointer(new TimelineModel(this, room_id))); -} - -void -TimelineViewManager::setHistoryView(const QString &room_id) -{ - nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - - auto room = models.find(room_id); - if (room != models.end()) { - timeline_ = room.value().data(); - emit activeTimelineChanged(timeline_); - nhlog::ui()->info("Activated room {}", room_id.toStdString()); - } -} - -void -TimelineViewManager::openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) 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(), 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; - } - - 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()); - }); -} - -void -TimelineViewManager::updateReadReceipts(const QString &room_id, - const std::vector &event_ids) -{ - auto room = models.find(room_id); - if (room != models.end()) { - room.value()->markEventsAsRead(event_ids); - } -} - -void -TimelineViewManager::initWithMessages(const std::map &msgs) -{ - for (const auto &e : msgs) { - addRoom(e.first); - - models.value(e.first)->addEvents(e.second); - } -} - -void -TimelineViewManager::queueTextMessage(const QString &msg) -{ - mtx::events::msg::Text text = {}; - text.body = msg.trimmed().toStdString(); - text.format = "org.matrix.custom.html"; - text.formatted_body = utils::markdownToHtml(msg).toStdString(); - - if (timeline_) - timeline_->sendMessage(text); -} - -void -TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) -{ - mtx::events::msg::Text text = {}; - - QString body; - bool firstLine = true; - for (const auto &line : related.quoted_body.split("\n")) { - if (firstLine) { - firstLine = false; - body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); - } else { - body = QString("%1\n> %2\n").arg(body).arg(line); - } - } - - text.body = QString("%1\n%2").arg(body).arg(reply).toStdString(); - text.format = "org.matrix.custom.html"; - text.formatted_body = - utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString(); - text.relates_to.in_reply_to.event_id = related.related_event; - - if (timeline_) - timeline_->sendMessage(text); -} - -void -TimelineViewManager::queueEmoteMessage(const QString &msg) -{ - auto html = utils::markdownToHtml(msg); - - mtx::events::msg::Emote emote; - emote.body = msg.trimmed().toStdString(); - - if (html != msg.trimmed().toHtmlEscaped()) - emote.formatted_body = html.toStdString(); - - if (timeline_) - timeline_->sendMessage(emote); -} - -void -TimelineViewManager::queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions) -{ - mtx::events::msg::Image image; - image.info.mimetype = mime.toStdString(); - image.info.size = dsize; - image.body = filename.toStdString(); - image.url = url.toStdString(); - image.info.h = dimensions.height(); - image.info.w = dimensions.width(); - models.value(roomid)->sendMessage(image); -} - -void -TimelineViewManager::queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::File file; - file.info.mimetype = mime.toStdString(); - file.info.size = dsize; - file.body = filename.toStdString(); - file.url = url.toStdString(); - models.value(roomid)->sendMessage(file); -} - -void -TimelineViewManager::queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = mime.toStdString(); - audio.info.size = dsize; - audio.body = filename.toStdString(); - audio.url = url.toStdString(); - models.value(roomid)->sendMessage(audio); -} - -void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::Video video; - video.info.mimetype = mime.toStdString(); - video.info.size = dsize; - video.body = filename.toStdString(); - video.url = url.toStdString(); - models.value(roomid)->sendMessage(video); -} diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h deleted file mode 100644 index 691c8ddb..00000000 --- a/src/timeline2/TimelineViewManager.h +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include "Cache.h" -#include "Logging.h" -#include "TimelineModel.h" -#include "Utils.h" - -// temporary for stubs -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - -class MxcImageProvider; -class ColorImageProvider; - -class TimelineViewManager : public QObject -{ - Q_OBJECT - - Q_PROPERTY( - TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) - -public: - TimelineViewManager(QWidget *parent = 0); - QWidget *getWidget() const { return container; } - - void sync(const mtx::responses::Rooms &rooms); - void addRoom(const QString &room_id); - - void clearAll() { models.clear(); } - - Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } - 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); - } - -signals: - void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(QString roomid, const DescInfo &info); - void activeTimelineChanged(TimelineModel *timeline); - void mediaCached(QString mxcUrl, QString cacheUrl); - -public slots: - void updateReadReceipts(const QString &room_id, const std::vector &event_ids); - void initWithMessages(const std::map &msgs); - - void setHistoryView(const QString &room_id); - void updateColorPalette(); - - void queueTextMessage(const QString &msg); - void queueReplyMessage(const QString &reply, const RelatedInfo &related); - void queueEmoteMessage(const QString &msg); - void queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions); - void queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - -private: -#ifdef USE_QUICK_VIEW - QQuickView *view; -#else - QQuickWidget *view; -#endif - QWidget *container; - TimelineModel *timeline_ = nullptr; - MxcImageProvider *imgProvider; - ColorImageProvider *colorImgProvider; - - QHash> models; -}; - -#pragma GCC diagnostic pop From 562169965ce68cadbf8214e084477c60ddfdde0b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 9 Nov 2019 03:30:17 +0100 Subject: [PATCH 86/94] Show only messages in room list --- src/timeline/TimelineModel.cpp | 37 +++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index ab7d3d47..9cae4608 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -552,17 +552,40 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) updateLastMessage(); } +template +auto +isMessage(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, bool> +{ + return true; +} + +template +auto +isMessage(const mtx::events::Event &) +{ + return false; +} + void TimelineModel::updateLastMessage() { - auto event = events.value(eventOrder.back()); - if (auto e = boost::get>(&event)) { - event = decryptEvent(*e).event; - } + for (auto it = eventOrder.rbegin(); it != eventOrder.rend(); ++it) { + auto event = events.value(*it); + if (auto e = boost::get>( + &event)) { + event = decryptEvent(*e).event; + } - auto description = utils::getMessageDescription( - event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); - emit manager_->updateRoomsLastMessage(room_id_, description); + if (!boost::apply_visitor([](const auto &e) -> bool { return isMessage(e); }, + event)) + continue; + + auto description = utils::getMessageDescription( + event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); + emit manager_->updateRoomsLastMessage(room_id_, description); + return; + } } std::vector From 165935683948a3b5f5c37bf124efec1249f679ae Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 9 Nov 2019 14:09:10 +0100 Subject: [PATCH 87/94] Update translations --- Makefile | 2 +- resources/langs/nheko_de.ts | 321 +++++++++++++++++++------------ resources/langs/nheko_el.ts | 301 ++++++++++++++++++----------- resources/langs/nheko_en.ts | 333 ++++++++++++++++++++------------- resources/langs/nheko_fi.ts | 315 +++++++++++++++++++------------ resources/langs/nheko_fr.ts | 301 ++++++++++++++++++----------- resources/langs/nheko_nl.ts | 301 ++++++++++++++++++----------- resources/langs/nheko_pl.ts | 305 +++++++++++++++++++----------- resources/langs/nheko_ru.ts | 305 +++++++++++++++++++----------- resources/langs/nheko_zh_CN.ts | 305 +++++++++++++++++++----------- 10 files changed, 1741 insertions(+), 1048 deletions(-) diff --git a/Makefile b/Makefile index 2f688d3b..7f603dcb 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ update-translations: -locations relative \ -Iinclude/dialogs \ -Iinclude \ - src/ -ts resources/langs/nheko_*.ts -no-obsolete + src/ resources/qml/ -ts resources/langs/nheko_*.ts -no-obsolete clean: rm -rf build diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts index e92bf966..879551bd 100644 --- a/resources/langs/nheko_de.ts +++ b/resources/langs/nheko_de.ts @@ -1,14 +1,6 @@ - - AudioItem - - - Save File - In Datei speichern - - ChatPage @@ -32,7 +24,7 @@ Hochladen der Videodatei fehlgeschlagen. Bitte versuche es erneut. - + Failed to restore OLM account. Please login again. Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein. @@ -42,18 +34,18 @@ 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. - + Please try to login again: %1 Bitte melde dich erneut an: %1 - + Room creation failed: %1 Raum konnte nicht erstellt werden: %1 @@ -116,19 +108,11 @@ - FileItem + EncryptionIndicator - - Save File - Datei speichern - - - - ImageItem - - - Save image - Bild speichern + + Encrypted + Verschlüsselt @@ -200,7 +184,7 @@ MemberList - + Room members Teilnehmerliste @@ -210,6 +194,14 @@ OK + + Placeholder + + + unimplemented event: + unimplementiertes event: + + QuickSwitcher @@ -218,6 +210,14 @@ Raum suchen… + + Redacted + + + redacted + gelöscht + + RegisterPage @@ -277,7 +277,7 @@ RoomInfo - + no version stored keine Version gespeichert @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Raum verlassen - + Accept Akzeptieren @@ -331,25 +331,25 @@ StatusIndicator - - Encrypted - Verschlüsselt + + Failed + Fehlgeschlagen - - Delivered - Erhalten - - - - Seen - Gelesen - - - + Sent Gesendet + + + Received + Empfangen + + + + Read + Gelesen + TextInputWidget @@ -391,32 +391,9 @@ - TimelineItem + TimelineModel - - Message redaction failed: %1 - Nachricht zurückziehen fehlgeschlagen: %1 - - - - Reply - Antworten - - - - Options - Optionen - - - - TimelineView - - - Encryption is enabled - Verschlüsselung aktiv - - - + -- 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) -- @@ -440,16 +417,90 @@ -- 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 + + + + TimelineRow + + + Reply + Antworten + + + + Options + Optionen + + + + Read receipts + Lesebestätigungen + + + + Mark as read + Als gelesen markieren + + + + View raw message + Zeige rohen Nachrichteninhalt + + + + Redact message + Nachricht löschen + + + + Save as + Speichern als... + + + + TimelineView + + + No room open + Kein Raum geöffnet + + + + TimelineViewManager + + + Save image + Bild speichern + + + + Save video + Video speichern + + + + Save audio + Audiodatei speichern + + + + Save file + Datei speichern + TopRoomBar - + Room options Raumoptionen @@ -515,7 +566,7 @@ UserSettingsPage - + Minimize to tray Ins Benachrichtigungsfeld minimieren @@ -529,6 +580,11 @@ Group's sidebar Gruppen-Seitenleiste + + + Circular Avatars + Runde Profilbilder + Typing notifications @@ -605,7 +661,7 @@ ALLGEMEINES - + Open Sessions File Öffne Sessions Datei @@ -825,7 +881,7 @@ Medien-Größe: %2 dialogs::ReadReceipts - + Read receipts Lesebestätigungen @@ -951,7 +1007,7 @@ Medien-Größe: %2 Aktivierung der Verschlüsselung fehlgeschlagen: %1 - + Select an avatar Wähle einen Avatar @@ -977,19 +1033,6 @@ Medien-Größe: %2 Hochladen der Bilddatei fehlgeschlagen: %s - - dialogs::UserMentions - - - This Room - Dieser Raum - - - - All Rooms - Alle Räume - - dialogs::UserProfile @@ -1013,7 +1056,7 @@ Medien-Größe: %2 Gespräch beginnen - + Devices Geräte @@ -1064,69 +1107,103 @@ Medien-Größe: %2 message-description sent: - - %1 an audio clip - %1 einen Audioclip + + You sent an audio clip + Du hast eine Audiodatei gesendet. - %1 an image - %1 ein Bild + %1 sent an audio clip + %1 hat eine Audiodatei gesendet. + + + + You sent an image + Du hast ein Bild gesendet. - %1 a file - %1 eine Datei + %1 sent an image + %1 hat ein Bild gesendet. + + + + You sent a file + Du hast eine Datei gesendet. - %1 a video clip - %1 einen Videoclip + %1 sent a file + %1 hat eine Datei gesendet. + + + + You sent a video + Du hast ein Video gesendet. - %1 a sticker - %1 einen Sticker + %1 sent a video + %1 hat ein Video gesendet. + + + + You sent a sticker + Du hast einen Sticker gesendet. - %1 a notification - 1% eine Benachrichtigung + %1 sent a sticker + %1 hat einen Sticker gesendet. + + + + You sent a notification + Du hast eine Benachrichtigung gesendet. + + + + %1 sent a notification + %1 hat eine Benachrichtigung gesendet. + + + + You: %1 + Du: %1 + + + + %1: %2 + %1: %2 - %1 an encrypted message - 1% eine verschüsselte Nachricht + You sent an encrypted message + Du hast eine verschlüsselte Nachricht gesendet. + + + + %1 sent an encrypted message + %1 hat eine verschlüsselte Nachricht gesendet. - message-description: + popups::UserMentions - - sent - For when someone else is the sender - + + This Room + Dieser Raum - - - message-description: - - sent - For when you are the sender - + + All Rooms + Alle Räume utils - - - You - Du - - - + sent a file. @@ -1146,7 +1223,7 @@ Medien-Größe: %2 - + Unknown Message Type Unbekannter Nachrichtentyp diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts index 700c3d57..e9c70da0 100644 --- a/resources/langs/nheko_el.ts +++ b/resources/langs/nheko_el.ts @@ -1,14 +1,6 @@ - - AudioItem - - - Save File - Αποθήκευση - - ChatPage @@ -32,7 +24,7 @@ - + Failed to restore OLM account. Please login again. @@ -42,18 +34,18 @@ - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - + Please try to login again: %1 - + Room creation failed: %1 @@ -116,19 +108,11 @@ - FileItem + EncryptionIndicator - - Save File - Αποθήκευση - - - - ImageItem - - - Save image - Αποθήκευση Εικόνας + + Encrypted + @@ -200,7 +184,7 @@ MemberList - + Room members Μέλη @@ -210,6 +194,14 @@ + + Placeholder + + + unimplemented event: + + + QuickSwitcher @@ -218,6 +210,14 @@ Αναζήτηση συνομιλίας... + + Redacted + + + redacted + + + RegisterPage @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Βγές - + Accept Αποδοχή @@ -331,25 +331,25 @@ StatusIndicator - - Encrypted + + Failed - - Delivered - - - - - Seen - - - - + Sent + + + Received + + + + + Read + + TextInputWidget @@ -391,32 +391,9 @@ - TimelineItem + TimelineModel - - Message redaction failed: %1 - - - - - Reply - - - - - Options - - - - - TimelineView - - - Encryption is enabled - - - - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted @@ -440,16 +417,90 @@ - + -- 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 + + + + + TimelineRow + + + Reply + + + + + Options + + + + + Read receipts + + + + + Mark as read + + + + + View raw message + + + + + Redact message + + + + + Save as + + + + + TimelineView + + + No room open + + + + + TimelineViewManager + + + Save image + Αποθήκευση Εικόνας + + + + Save video + + + + + Save audio + + + + + Save file + + TopRoomBar - + Room options @@ -515,7 +566,7 @@ UserSettingsPage - + Minimize to tray Ελαχιστοποίηση @@ -529,6 +580,11 @@ Group's sidebar + + + Circular Avatars + + Typing notifications @@ -605,7 +661,7 @@ ΓΕΝΙΚΑ - + Open Sessions File @@ -823,7 +879,7 @@ Media size: %2 dialogs::ReadReceipts - + Read receipts @@ -949,7 +1005,7 @@ Media size: %2 - + Select an avatar @@ -975,19 +1031,6 @@ Media size: %2 - - dialogs::UserMentions - - - This Room - - - - - All Rooms - - - dialogs::UserProfile @@ -1011,7 +1054,7 @@ Media size: %2 - + Devices @@ -1062,69 +1105,103 @@ Media size: %2 message-description sent: - - %1 an audio clip + + You sent an audio clip - %1 an image + %1 sent an audio clip + + + + + You sent an image - %1 a file + %1 sent an image + + + + + You sent a file - %1 a video clip + %1 sent a file + + + + + You sent a video - %1 a sticker + %1 sent a video + + + + + You sent a sticker - %1 a notification + %1 sent a sticker + + + + + You sent a notification + + + + + %1 sent a notification + + + + + You: %1 + + + + + %1: %2 - %1 an encrypted message + You sent an encrypted message + + + + + %1 sent an encrypted message - message-description: + popups::UserMentions - - sent - For when someone else is the sender + + This Room - - - message-description: - - sent - For when you are the sender + + All Rooms utils - - - You - - - - + sent a file. @@ -1144,7 +1221,7 @@ Media size: %2 - + Unknown Message Type diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts index edb95313..cb2ef1c7 100644 --- a/resources/langs/nheko_en.ts +++ b/resources/langs/nheko_en.ts @@ -1,14 +1,6 @@ - - AudioItem - - - Save File - Save File - - ChatPage @@ -32,7 +24,7 @@ Failed to upload video. Please try again. - + Failed to restore OLM account. Please login again. Failed to restore OLM account. Please login again. @@ -42,18 +34,18 @@ 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. - + Please try to login again: %1 Please try to login again: %1 - + Room creation failed: %1 Room creation failed: %1 @@ -116,19 +108,11 @@ - FileItem + EncryptionIndicator - - Save File - Save File - - - - ImageItem - - - Save image - Save image + + Encrypted + @@ -200,7 +184,7 @@ MemberList - + Room members Room members @@ -210,6 +194,14 @@ OK + + Placeholder + + + unimplemented event: + + + QuickSwitcher @@ -218,6 +210,14 @@ Search for a room… + + Redacted + + + redacted + + + RegisterPage @@ -277,7 +277,7 @@ RoomInfo - + no version stored no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Leave room - + Accept Accept @@ -331,24 +331,24 @@ StatusIndicator - - Encrypted - Encrypted + + Failed + - - Delivered - Delivered - - - - Seen - Seen - - - + Sent - Sent + + + + + Received + + + + + Read + @@ -391,65 +391,116 @@ - TimelineItem + TimelineModel - - Message redaction failed: %1 - Message redaction failed: %1 - - - - Reply - Reply - - - - Options - Options - - - - TimelineView - - - Encryption is enabled - Encryption is enabled - - - + -- 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) -- + -- Encrypted Event (No keys found for decryption) -- -- Decryption Error (failed to communicate with DB) -- Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session. - -- Decryption Error (failed to communicate with DB) -- + -- Decryption Error (failed to communicate with DB) -- -- Decryption Error (failed to retrieve megolm keys from db) -- Placeholder, when the message can't be decrypted, because the DB access failed. - -- Decryption Error (failed to retrieve megolm keys from db) -- + -- Decryption Error (failed to retrieve megolm keys from db) -- -- Decryption Error (%1) -- Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1 - -- Decryption Error (%1) -- + -- 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) -- + -- Encrypted Event (Unknown event type) -- + + + + Message redaction failed: %1 + Message redaction failed: %1 + + + + TimelineRow + + + Reply + + + + + Options + + + + + Read receipts + Read receipts + + + + Mark as read + + + + + View raw message + + + + + Redact message + + + + + Save as + + + + + TimelineView + + + No room open + + + + + TimelineViewManager + + + Save image + Save image + + + + Save video + + + + + Save audio + + + + + Save file + TopRoomBar - + Room options Room options @@ -515,7 +566,7 @@ UserSettingsPage - + Minimize to tray Minimize to tray @@ -529,6 +580,11 @@ Group's sidebar Group's sidebar + + + Circular Avatars + + Typing notifications @@ -605,7 +661,7 @@ GENERAL - + Open Sessions File Open Sessions File @@ -825,7 +881,7 @@ Media size: %2 dialogs::ReadReceipts - + Read receipts Read receipts @@ -953,7 +1009,7 @@ Media size: %2 Failed to enable encryption: %1 - + Select an avatar Select an avatar @@ -979,19 +1035,6 @@ Media size: %2 Failed to upload image: %s - - dialogs::UserMentions - - - This Room - This Room - - - - All Rooms - All Rooms - - dialogs::UserProfile @@ -1015,7 +1058,7 @@ Media size: %2 Start a conversation - + Devices Devices @@ -1066,69 +1109,103 @@ Media size: %2 message-description sent: - - %1 an audio clip - %1 an audio clip + + You sent an audio clip + - %1 an image - %1 an image + %1 sent an audio clip + + + + + You sent an image + - %1 a file - %1 a file + %1 sent an image + + + + + You sent a file + - %1 a video clip - %1 a video clip + %1 sent a file + + + + + You sent a video + - %1 a sticker - %1 a sticker + %1 sent a video + + + + + You sent a sticker + - %1 a notification - %1 a notification + %1 sent a sticker + + + + + You sent a notification + + + + + %1 sent a notification + + + + + You: %1 + + + + + %1: %2 + - %1 an encrypted message - %1 an encrypted message + You sent an encrypted message + + + + + %1 sent an encrypted message + - message-description: + popups::UserMentions - - sent - For when someone else is the sender - sent + + This Room + This Room - - - message-description: - - sent - For when you are the sender - sent + + All Rooms + All Rooms utils - - - You - You - - - + sent a file. sent a file. @@ -1148,7 +1225,7 @@ Media size: %2 sent a video. - + Unknown Message Type Unknown Message Type diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts index 89eb33b7..76bf7064 100644 --- a/resources/langs/nheko_fi.ts +++ b/resources/langs/nheko_fi.ts @@ -1,14 +1,6 @@ - - AudioItem - - - Save File - Tallenna tiedosto - - ChatPage @@ -32,7 +24,7 @@ Videon lähettäminen epäonnistui. Ole hyvä ja yritä uudelleen. - + Failed to restore OLM account. Please login again. OLM-tilin palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen. @@ -42,18 +34,18 @@ 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. - + Please try to login again: %1 Ole hyvä ja yritä kirjautua sisään uudelleen: %1 - + Room creation failed: %1 Huoneen luominen epäonnistui: %1 @@ -116,19 +108,11 @@ - FileItem + EncryptionIndicator - - Save File - Tallenna tiedosto - - - - ImageItem - - - Save image - Tallenna kuva + + Encrypted + @@ -200,7 +184,7 @@ MemberList - + Room members Huoneen jäsenet @@ -210,6 +194,14 @@ OK + + Placeholder + + + unimplemented event: + + + QuickSwitcher @@ -218,6 +210,14 @@ Etsi huonetta… + + Redacted + + + redacted + + + RegisterPage @@ -277,7 +277,7 @@ RoomInfo - + no version stored ei tallennettua versiota @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Poistu huoneesta - + Accept Hyväksy @@ -331,24 +331,24 @@ StatusIndicator - - Encrypted - Salattu + + Failed + - - Delivered - Toimitettu - - - - Seen - Luettu - - - + Sent - Lähetetty + + + + + Received + + + + + Read + @@ -391,65 +391,116 @@ - TimelineItem + TimelineModel - - Message redaction failed: %1 - Viestin poisto epäonnistui: %1 - - - - Reply - Vastaa - - - - Options - Asetukset - - - - TimelineView - - - Encryption is enabled - Salaus on käytössä - - - + -- 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) -- + -- Salattu viesti (salauksen purkuavaimia ei löydetty) -- -- Decryption Error (failed to communicate with DB) -- Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session. - -- Virhe purkaessa salausta (tietokannan kanssa kommunikointi epäonnistui) -- + -- Virhe purkaessa salausta (tietokannan kanssa kommunikointi epäonnistui) -- -- Decryption Error (failed to retrieve megolm keys from db) -- Placeholder, when the message can't be decrypted, because the DB access failed. - -- Virhe purkaessa salausta (megolm-avaimien hakeminen tietokannasta epäonnistui) -- + -- Virhe purkaessa salausta (megolm-avaimien hakeminen tietokannasta epäonnistui) -- -- Decryption Error (%1) -- Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1 - -- Virhe purkaessa salausta (%1) -- + -- 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) -- + -- Salattu viesti (tuntematon viestityyppi) -- + + + + Message redaction failed: %1 + Viestin poisto epäonnistui: %1 + + + + TimelineRow + + + Reply + + + + + Options + + + + + Read receipts + Lukukuittaukset + + + + Mark as read + + + + + View raw message + + + + + Redact message + + + + + Save as + + + + + TimelineView + + + No room open + + + + + TimelineViewManager + + + Save image + Tallenna kuva + + + + Save video + + + + + Save audio + + + + + Save file + TopRoomBar - + Room options Huonevaihtoehdot @@ -515,7 +566,7 @@ UserSettingsPage - + Minimize to tray Pienennä ilmoitusalueelle @@ -529,6 +580,11 @@ Group's sidebar Ryhmäsivupalkki + + + Circular Avatars + + Typing notifications @@ -605,7 +661,7 @@ YLEISET ASETUKSET - + Open Sessions File Avaa Istuntoavaintiedosto @@ -825,7 +881,7 @@ Median koko: %2 dialogs::ReadReceipts - + Read receipts Lukukuittaukset @@ -953,7 +1009,7 @@ Median koko: %2 Salauksen aktivointi epäonnistui: %1 - + Select an avatar Valitse profiilikuva @@ -979,19 +1035,6 @@ Median koko: %2 Kuvan lähetys epäonnistui: %s - - dialogs::UserMentions - - - This Room - - - - - All Rooms - - - dialogs::UserProfile @@ -1015,7 +1058,7 @@ Median koko: %2 Aloita keskustelu - + Devices Laitteet @@ -1066,69 +1109,103 @@ Median koko: %2 message-description sent: - - %1 an audio clip + + You sent an audio clip - %1 an image + %1 sent an audio clip + + + + + You sent an image - %1 a file + %1 sent an image + + + + + You sent a file - %1 a video clip + %1 sent a file + + + + + You sent a video - %1 a sticker + %1 sent a video + + + + + You sent a sticker - %1 a notification + %1 sent a sticker + + + + + You sent a notification + + + + + %1 sent a notification + + + + + You: %1 + + + + + %1: %2 - %1 an encrypted message + You sent an encrypted message + + + + + %1 sent an encrypted message - message-description: + popups::UserMentions - - sent - For when someone else is the sender + + This Room - - - message-description: - - sent - For when you are the sender + + All Rooms utils - - - You - Sinä - - - + sent a file. @@ -1148,7 +1225,7 @@ Median koko: %2 - + Unknown Message Type diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts index 42f82b0f..30ff8599 100644 --- a/resources/langs/nheko_fr.ts +++ b/resources/langs/nheko_fr.ts @@ -1,14 +1,6 @@ - - AudioItem - - - Save File - Enregistrer le fichier - - ChatPage @@ -32,7 +24,7 @@ - + Failed to restore OLM account. Please login again. @@ -42,18 +34,18 @@ - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - + Please try to login again: %1 - + Room creation failed: %1 @@ -116,19 +108,11 @@ - FileItem + EncryptionIndicator - - Save File - Enregistrer le fichier - - - - ImageItem - - - Save image - Enregistrer l'image + + Encrypted + @@ -200,7 +184,7 @@ MemberList - + Room members Membres du salon @@ -210,6 +194,14 @@ + + Placeholder + + + unimplemented event: + + + QuickSwitcher @@ -218,6 +210,14 @@ Chercher un salon… + + Redacted + + + redacted + + + RegisterPage @@ -278,7 +278,7 @@ RoomInfo - + no version stored @@ -286,12 +286,12 @@ RoomInfoListItem - + Leave room Quitter le salon - + Accept Accepter @@ -332,25 +332,25 @@ StatusIndicator - - Encrypted + + Failed - - Delivered - - - - - Seen - - - - + Sent + + + Received + + + + + Read + + TextInputWidget @@ -392,32 +392,9 @@ - TimelineItem + TimelineModel - - Message redaction failed: %1 - - - - - Reply - - - - - Options - - - - - TimelineView - - - Encryption is enabled - - - - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted @@ -441,16 +418,90 @@ - + -- 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 + + + + + TimelineRow + + + Reply + + + + + Options + + + + + Read receipts + Accusés de lecture + + + + Mark as read + + + + + View raw message + + + + + Redact message + + + + + Save as + + + + + TimelineView + + + No room open + + + + + TimelineViewManager + + + Save image + Enregistrer l'image + + + + Save video + + + + + Save audio + + + + + Save file + + TopRoomBar - + Room options @@ -516,7 +567,7 @@ UserSettingsPage - + Minimize to tray Réduire à la barre des tâches @@ -530,6 +581,11 @@ Group's sidebar Barre latérale des groupes + + + Circular Avatars + + Typing notifications @@ -606,7 +662,7 @@ GÉNÉRAL - + Open Sessions File @@ -826,7 +882,7 @@ Taille du média : %2 dialogs::ReadReceipts - + Read receipts Accusés de lecture @@ -952,7 +1008,7 @@ Taille du média : %2 - + Select an avatar @@ -978,19 +1034,6 @@ Taille du média : %2 - - dialogs::UserMentions - - - This Room - - - - - All Rooms - - - dialogs::UserProfile @@ -1014,7 +1057,7 @@ Taille du média : %2 - + Devices @@ -1065,69 +1108,103 @@ Taille du média : %2 message-description sent: - - %1 an audio clip + + You sent an audio clip - %1 an image + %1 sent an audio clip + + + + + You sent an image - %1 a file + %1 sent an image + + + + + You sent a file - %1 a video clip + %1 sent a file + + + + + You sent a video - %1 a sticker + %1 sent a video + + + + + You sent a sticker - %1 a notification + %1 sent a sticker + + + + + You sent a notification + + + + + %1 sent a notification + + + + + You: %1 + + + + + %1: %2 - %1 an encrypted message + You sent an encrypted message + + + + + %1 sent an encrypted message - message-description: + popups::UserMentions - - sent - For when someone else is the sender + + This Room - - - message-description: - - sent - For when you are the sender + + All Rooms utils - - - You - - - - + sent a file. @@ -1147,7 +1224,7 @@ Taille du média : %2 - + Unknown Message Type diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts index 53840f82..1c8a83c0 100644 --- a/resources/langs/nheko_nl.ts +++ b/resources/langs/nheko_nl.ts @@ -1,14 +1,6 @@ - - AudioItem - - - Save File - Bestand opslaan - - ChatPage @@ -32,7 +24,7 @@ - + Failed to restore OLM account. Please login again. @@ -42,18 +34,18 @@ - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - + Please try to login again: %1 - + Room creation failed: %1 @@ -116,19 +108,11 @@ - FileItem + EncryptionIndicator - - Save File - Bestand opslaan - - - - ImageItem - - - Save image - Afbeelding opslaan + + Encrypted + @@ -200,7 +184,7 @@ MemberList - + Room members Kamerleden @@ -210,6 +194,14 @@ + + Placeholder + + + unimplemented event: + + + QuickSwitcher @@ -218,6 +210,14 @@ Zoek een kamer... + + Redacted + + + redacted + + + RegisterPage @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Kamer verlaten - + Accept Accepteren @@ -331,25 +331,25 @@ StatusIndicator - - Encrypted + + Failed - - Delivered - - - - - Seen - - - - + Sent + + + Received + + + + + Read + + TextInputWidget @@ -391,32 +391,9 @@ - TimelineItem + TimelineModel - - Message redaction failed: %1 - - - - - Reply - - - - - Options - - - - - TimelineView - - - Encryption is enabled - - - - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted @@ -440,16 +417,90 @@ - + -- 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 + + + + + TimelineRow + + + Reply + + + + + Options + + + + + Read receipts + Leesbevestigingen + + + + Mark as read + + + + + View raw message + + + + + Redact message + + + + + Save as + + + + + TimelineView + + + No room open + + + + + TimelineViewManager + + + Save image + Afbeelding opslaan + + + + Save video + + + + + Save audio + + + + + Save file + + TopRoomBar - + Room options @@ -515,7 +566,7 @@ UserSettingsPage - + Minimize to tray Minimaliseren naar systeemvak @@ -529,6 +580,11 @@ Group's sidebar Zijbalk van groep + + + Circular Avatars + + Typing notifications @@ -605,7 +661,7 @@ ALGEMEEN - + Open Sessions File @@ -825,7 +881,7 @@ Mediagrootte: %2 dialogs::ReadReceipts - + Read receipts Leesbevestigingen @@ -951,7 +1007,7 @@ Mediagrootte: %2 - + Select an avatar @@ -977,19 +1033,6 @@ Mediagrootte: %2 - - dialogs::UserMentions - - - This Room - - - - - All Rooms - - - dialogs::UserProfile @@ -1013,7 +1056,7 @@ Mediagrootte: %2 - + Devices @@ -1064,69 +1107,103 @@ Mediagrootte: %2 message-description sent: - - %1 an audio clip + + You sent an audio clip - %1 an image + %1 sent an audio clip + + + + + You sent an image - %1 a file + %1 sent an image + + + + + You sent a file - %1 a video clip + %1 sent a file + + + + + You sent a video - %1 a sticker + %1 sent a video + + + + + You sent a sticker - %1 a notification + %1 sent a sticker + + + + + You sent a notification + + + + + %1 sent a notification + + + + + You: %1 + + + + + %1: %2 - %1 an encrypted message + You sent an encrypted message + + + + + %1 sent an encrypted message - message-description: + popups::UserMentions - - sent - For when someone else is the sender + + This Room - - - message-description: - - sent - For when you are the sender + + All Rooms utils - - - You - - - - + sent a file. @@ -1146,7 +1223,7 @@ Mediagrootte: %2 - + Unknown Message Type diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts index f4f98dbb..6c3b2abd 100644 --- a/resources/langs/nheko_pl.ts +++ b/resources/langs/nheko_pl.ts @@ -1,14 +1,6 @@ - - AudioItem - - - Save File - Zapisz plik - - ChatPage @@ -32,7 +24,7 @@ Nie udało się wysłać filmu. Spróbuj ponownie. - + Failed to restore OLM account. Please login again. Nie udało się przywrócić konta OLM. Spróbuj zalogować się ponownie. @@ -42,18 +34,18 @@ 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. - + Please try to login again: %1 Spróbuj zalogować się ponownie: %1 - + Room creation failed: %1 Tworzenie pokoju nie powiodło się: %1 @@ -116,19 +108,11 @@ - FileItem + EncryptionIndicator - - Save File - Zapisz plik - - - - ImageItem - - - Save image - Zapisz obraz + + Encrypted + @@ -200,7 +184,7 @@ MemberList - + Room members Członkowie pokoju @@ -210,6 +194,14 @@ + + Placeholder + + + unimplemented event: + + + QuickSwitcher @@ -218,6 +210,14 @@ Wyszukaj pokoju… + + Redacted + + + redacted + + + RegisterPage @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Opuść pokój - + Accept Akceptuj @@ -331,24 +331,24 @@ StatusIndicator - - Encrypted - Szyfrowana + + Failed + - - Delivered - Dostarczono - - - - Seen - Wyświetlona - - - + Sent - Wysłana + + + + + Received + + + + + Read + @@ -391,32 +391,9 @@ - TimelineItem + TimelineModel - - Message redaction failed: %1 - Redagowanie wiadomości nie powiodło się: %1 - - - - Reply - - - - - Options - - - - - TimelineView - - - Encryption is enabled - Szyfrowanie jest włączone - - - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted @@ -440,16 +417,90 @@ - + -- 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 + + + + TimelineRow + + + Reply + + + + + Options + + + + + Read receipts + Potwierdzenia przeczytania + + + + Mark as read + + + + + View raw message + + + + + Redact message + + + + + Save as + + + + + TimelineView + + + No room open + + + + + TimelineViewManager + + + Save image + Zapisz obraz + + + + Save video + + + + + Save audio + + + + + Save file + + TopRoomBar - + Room options Ustawienia pokoju @@ -516,7 +567,7 @@ UserSettingsPage - + Minimize to tray Zminimalizuj do paska zadań @@ -530,6 +581,11 @@ Group's sidebar Pasek boczny grupy + + + Circular Avatars + + Typing notifications @@ -606,7 +662,7 @@ OGÓLNE - + Open Sessions File @@ -826,7 +882,7 @@ Rozmiar multimediów: %2 dialogs::ReadReceipts - + Read receipts Potwierdzenia przeczytania @@ -955,7 +1011,7 @@ Rozmiar multimediów: %2 Nie udało się włączyć szyfrowania: %1 - + Select an avatar Wybierz awatar @@ -981,19 +1037,6 @@ Rozmiar multimediów: %2 Nie udało się wysłać obrazu: %s - - dialogs::UserMentions - - - This Room - - - - - All Rooms - - - dialogs::UserProfile @@ -1017,7 +1060,7 @@ Rozmiar multimediów: %2 Rozpocznij rozmowę - + Devices Urządzenia @@ -1068,69 +1111,103 @@ Rozmiar multimediów: %2 message-description sent: - - %1 an audio clip + + You sent an audio clip - %1 an image + %1 sent an audio clip + + + + + You sent an image - %1 a file + %1 sent an image + + + + + You sent a file - %1 a video clip + %1 sent a file + + + + + You sent a video - %1 a sticker + %1 sent a video + + + + + You sent a sticker - %1 a notification + %1 sent a sticker + + + + + You sent a notification + + + + + %1 sent a notification + + + + + You: %1 + + + + + %1: %2 - %1 an encrypted message + You sent an encrypted message + + + + + %1 sent an encrypted message - message-description: + popups::UserMentions - - sent - For when someone else is the sender + + This Room - - - message-description: - - sent - For when you are the sender + + All Rooms utils - - - You - - - - + sent a file. @@ -1150,7 +1227,7 @@ Rozmiar multimediów: %2 - + Unknown Message Type diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts index 04285c72..d5544cf8 100644 --- a/resources/langs/nheko_ru.ts +++ b/resources/langs/nheko_ru.ts @@ -1,14 +1,6 @@ - - AudioItem - - - Save File - Сохранить файл - - ChatPage @@ -32,7 +24,7 @@ Не удалось загрузить видео. Пожалуйста, попробуйте еще раз. - + Failed to restore OLM account. Please login again. Не удалось восстановить учетную запись OLM. Пожалуйста, войдите снова. @@ -42,18 +34,18 @@ Не удалось восстановить сохраненные данные. Пожалуйста, войдите снова. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Не удалось настроить ключи шифрования. Ответ сервера:%1 %2. Пожалуйста, попробуйте позже. - + Please try to login again: %1 Повторите попытку входа: %1 - + Room creation failed: %1 Не удалось создать комнату: %1 @@ -116,19 +108,11 @@ - FileItem + EncryptionIndicator - - Save File - Сохранить файл - - - - ImageItem - - - Save image - Сохранить изображение + + Encrypted + @@ -200,7 +184,7 @@ MemberList - + Room members Участники комнаты @@ -210,6 +194,14 @@ + + Placeholder + + + unimplemented event: + + + QuickSwitcher @@ -218,6 +210,14 @@ Поиск комнаты... + + Redacted + + + redacted + + + RegisterPage @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Покинуть комнату - + Accept Принять @@ -331,24 +331,24 @@ StatusIndicator - - Encrypted - Зашифровано + + Failed + - - Delivered - Доставлено - - - - Seen - Прочитано - - - + Sent - Отправлено + + + + + Received + + + + + Read + @@ -391,32 +391,9 @@ - TimelineItem + TimelineModel - - Message redaction failed: %1 - Ошибка редактирования сообщения: %1 - - - - Reply - - - - - Options - - - - - TimelineView - - - Encryption is enabled - Шифрование включено - - - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted @@ -440,16 +417,90 @@ - + -- 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 + + + + TimelineRow + + + Reply + + + + + Options + + + + + Read receipts + Подтверждать прочтение + + + + Mark as read + + + + + View raw message + + + + + Redact message + + + + + Save as + + + + + TimelineView + + + No room open + + + + + TimelineViewManager + + + Save image + Сохранить изображение + + + + Save video + + + + + Save audio + + + + + Save file + + TopRoomBar - + Room options Настройки комнаты @@ -516,7 +567,7 @@ UserSettingsPage - + Minimize to tray Сворачивать в системную панель @@ -530,6 +581,11 @@ Group's sidebar Боковая панель групп + + + Circular Avatars + + Typing notifications @@ -606,7 +662,7 @@ ГЛАВНОЕ - + Open Sessions File Открыть файл сеансов @@ -827,7 +883,7 @@ Media size: %2 dialogs::ReadReceipts - + Read receipts Подтверждать прочтение @@ -954,7 +1010,7 @@ Media size: %2 Не удалось включить шифрование: %1 - + Select an avatar Выберите аватар @@ -980,19 +1036,6 @@ Media size: %2 Не удалось загрузить изображение: %s - - dialogs::UserMentions - - - This Room - - - - - All Rooms - - - dialogs::UserProfile @@ -1016,7 +1059,7 @@ Media size: %2 Начать разговор - + Devices Устройства @@ -1067,69 +1110,103 @@ Media size: %2 message-description sent: - - %1 an audio clip + + You sent an audio clip - %1 an image + %1 sent an audio clip + + + + + You sent an image - %1 a file + %1 sent an image + + + + + You sent a file - %1 a video clip + %1 sent a file + + + + + You sent a video - %1 a sticker + %1 sent a video + + + + + You sent a sticker - %1 a notification + %1 sent a sticker + + + + + You sent a notification + + + + + %1 sent a notification + + + + + You: %1 + + + + + %1: %2 - %1 an encrypted message + You sent an encrypted message + + + + + %1 sent an encrypted message - message-description: + popups::UserMentions - - sent - For when someone else is the sender + + This Room - - - message-description: - - sent - For when you are the sender + + All Rooms utils - - - You - - - - + sent a file. @@ -1149,7 +1226,7 @@ Media size: %2 - + Unknown Message Type diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts index 1e539e64..57f49d43 100644 --- a/resources/langs/nheko_zh_CN.ts +++ b/resources/langs/nheko_zh_CN.ts @@ -1,14 +1,6 @@ - - AudioItem - - - Save File - 保存文件 - - ChatPage @@ -32,7 +24,7 @@ 上传视频失败。请重试。 - + Failed to restore OLM account. Please login again. 恢复 OLM 账户失败。请重新登录。 @@ -42,18 +34,18 @@ 恢复保存的数据失败。请重新登录。 - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - + Please try to login again: %1 请尝试再次登录:%1 - + Room creation failed: %1 创建聊天室失败:%1 @@ -116,19 +108,11 @@ - FileItem + EncryptionIndicator - - Save File - 保存文件 - - - - ImageItem - - - Save image - 保存图像 + + Encrypted + @@ -200,7 +184,7 @@ MemberList - + Room members 聊天室成员 @@ -210,6 +194,14 @@ + + Placeholder + + + unimplemented event: + + + QuickSwitcher @@ -218,6 +210,14 @@ 寻找一个聊天室... + + Redacted + + + redacted + + + RegisterPage @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room 离开聊天室 - + Accept 接受 @@ -331,24 +331,24 @@ StatusIndicator - - Encrypted - 加密的 + + Failed + - - Delivered - 已送达 - - - - Seen - 已阅读 - - - + Sent - 已发送 + + + + + Received + + + + + Read + @@ -391,32 +391,9 @@ - TimelineItem + TimelineModel - - Message redaction failed: %1 - 删除消息失败:%1 - - - - Reply - - - - - Options - - - - - TimelineView - - - Encryption is enabled - 加密已启用 - - - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted @@ -440,16 +417,90 @@ - + -- 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 + + + + TimelineRow + + + Reply + + + + + Options + + + + + Read receipts + 阅读回执 + + + + Mark as read + + + + + View raw message + + + + + Redact message + + + + + Save as + + + + + TimelineView + + + No room open + + + + + TimelineViewManager + + + Save image + 保存图像 + + + + Save video + + + + + Save audio + + + + + Save file + + TopRoomBar - + Room options 聊天室选项 @@ -514,7 +565,7 @@ UserSettingsPage - + Minimize to tray 最小化至托盘 @@ -528,6 +579,11 @@ Group's sidebar 群组侧边栏 + + + Circular Avatars + + Typing notifications @@ -604,7 +660,7 @@ 通用 - + Open Sessions File 打开会话文件 @@ -824,7 +880,7 @@ Media size: %2 dialogs::ReadReceipts - + Read receipts 阅读回执 @@ -951,7 +1007,7 @@ Media size: %2 启用加密失败:%1 - + Select an avatar 选择一个头像 @@ -977,19 +1033,6 @@ Media size: %2 上传图像失败:%s - - dialogs::UserMentions - - - This Room - - - - - All Rooms - - - dialogs::UserProfile @@ -1013,7 +1056,7 @@ Media size: %2 开始一个聊天 - + Devices 设备 @@ -1072,69 +1115,103 @@ Media size: %2 message-description sent: - - %1 an audio clip + + You sent an audio clip - %1 an image + %1 sent an audio clip + + + + + You sent an image - %1 a file + %1 sent an image + + + + + You sent a file - %1 a video clip + %1 sent a file + + + + + You sent a video - %1 a sticker + %1 sent a video + + + + + You sent a sticker - %1 a notification + %1 sent a sticker + + + + + You sent a notification + + + + + %1 sent a notification + + + + + You: %1 + + + + + %1: %2 - %1 an encrypted message + You sent an encrypted message + + + + + %1 sent an encrypted message - message-description: + popups::UserMentions - - sent - For when someone else is the sender + + This Room - - - message-description: - - sent - For when you are the sender + + All Rooms utils - - - You - - - - + sent a file. @@ -1154,7 +1231,7 @@ Media size: %2 - + Unknown Message Type From c424e397b01d8191568f951bdb754e1957681fb8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 10 Nov 2019 00:30:02 +0100 Subject: [PATCH 88/94] 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(-) 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 From 001c94865c98836b06c827ff890a5589dd97320d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 12 Nov 2019 15:10:59 +0000 Subject: [PATCH 89/94] Fix windows build No idea, why apply visitor doesn't work with temporaries? --- src/dialogs/RoomSettings.cpp | 2 +- src/timeline/TimelineModel.cpp | 8 +++++--- src/timeline/TimelineViewManager.cpp | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 00b034cc..25909cd8 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -488,7 +488,7 @@ RoomSettings::retrieveRoomInfo() usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString()); info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); setAvatar(); - } catch (const lmdb::error &e) { + } catch (const lmdb::error &) { nhlog::db()->warn("failed to retrieve room info from cache: {}", room_id_.toStdString()); } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 6b0057a4..39abbf6f 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -438,16 +438,17 @@ TimelineModel::data(const QModelIndex &index, int role) const boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event); for (int r = index.row() - 1; r > 0; r--) { + auto tempEv = events.value(eventOrder[r]); QDateTime prevDate = boost::apply_visitor( [](const auto &e) -> QDateTime { return eventTimestamp(e); }, - events.value(eventOrder[r])); + tempEv); prevDate.setTime(QTime()); if (prevDate != date) return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); QString prevUserId = boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, - events.value(eventOrder[r])); + tempEv); if (userId != prevUserId) break; } @@ -1313,7 +1314,8 @@ TimelineModel::processOnePendingMessage() QString txn_id_qstr = pending.first(); - boost::apply_visitor(SendMessageVisitor{txn_id_qstr, this}, events.value(txn_id_qstr)); + auto event = events.value(txn_id_qstr); + boost::apply_visitor(SendMessageVisitor{txn_id_qstr, this}, event); } void diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 06c42a39..39bdfcf4 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -200,7 +200,7 @@ TimelineViewManager::saveMedia(QString mxcUrl, if (!file.open(QIODevice::WriteOnly)) return; - file.write(QByteArray(data.data(), data.size())); + 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()); From cf88499ccb6709db3312cd675c87614389cc0aac Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 11 Nov 2019 19:58:20 +0100 Subject: [PATCH 90/94] Fix replies to encrypted events --- src/timeline/TimelineModel.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 39abbf6f..72c107d4 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -869,7 +869,11 @@ TimelineModel::decryptEvent(const mtx::events::EncryptedEvent>(&event)) { + event = decryptEvent(*e).event; + } + RelatedInfo related = boost::apply_visitor( [](const auto &ev) -> RelatedInfo { RelatedInfo related_ = {}; From 5429b425e97f3482e7e5510a8606659168f58b7d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 12 Nov 2019 17:46:09 +0100 Subject: [PATCH 91/94] Lint --- src/timeline/TimelineModel.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 72c107d4..7c78e552 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -440,15 +440,13 @@ TimelineModel::data(const QModelIndex &index, int role) const for (int r = index.row() - 1; r > 0; r--) { auto tempEv = events.value(eventOrder[r]); QDateTime prevDate = boost::apply_visitor( - [](const auto &e) -> QDateTime { return eventTimestamp(e); }, - tempEv); + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, tempEv); prevDate.setTime(QTime()); if (prevDate != date) return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); - QString prevUserId = - boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, - tempEv); + QString prevUserId = boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, tempEv); if (userId != prevUserId) break; } From 7bd875004f6995865a71a55facf56834c91bfb48 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 22 Nov 2019 16:36:45 +0100 Subject: [PATCH 92/94] Only mark messages as read, when room is active --- src/timeline/TimelineModel.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 7c78e552..11344e60 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -685,7 +685,8 @@ TimelineModel::setCurrentIndex(int index) currentId = indexToId(index); emit currentIndexChanged(index); - if (oldIndex < index && !pending.contains(currentId)) { + if (oldIndex < index && !pending.contains(currentId) && + ChatPage::instance()->isActiveWindow()) { readEvent(currentId.toStdString()); } } From 85aae9408b2bb8ebd64c91fdcb4eddc9aec49746 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 22 Nov 2019 16:37:43 +0100 Subject: [PATCH 93/94] Wrap text in pre tags --- resources/qml/delegates/TextMessage.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 990a3f5b..f984b32f 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -1,6 +1,6 @@ import ".." MatrixText { - text: model.formattedBody + text: model.formattedBody.replace("
", "
")
 	width: parent ? parent.width : undefined
 }

From 9fd279c020bba2f433a0f9862277bc59fd621130 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 22 Nov 2019 17:08:32 +0100
Subject: [PATCH 94/94] Show encryption enabled and use a non zero size for
 zero size vide

---
 resources/qml/TimelineView.qml                     |  2 +-
 resources/qml/delegates/MessageDelegate.qml        | 10 +++++++++-
 resources/qml/delegates/{Redacted.qml => Pill.qml} |  1 -
 resources/qml/delegates/PlayableMediaMessage.qml   |  2 +-
 resources/res.qrc                                  |  2 +-
 src/timeline/TimelineModel.cpp                     |  5 ++++-
 6 files changed, 16 insertions(+), 6 deletions(-)
 rename resources/qml/delegates/{Redacted.qml => Pill.qml} (91%)

diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 3bbaa020..a5520031 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -79,7 +79,7 @@ Item {
 				}
 			}
 
-			onAtYBeginningChanged: if (atYBeginning) model.fetchHistory()
+			onAtYBeginningChanged: if (atYBeginning) { chat.model.currentIndex = 0; chat.currentIndex = 0; model.fetchHistory(); }
 
 			function updatePosition() {
 				for (var y = chat.contentY + chat.height; y > chat.height; y -= 9) {
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 49209f68..e31321f9 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -39,7 +39,15 @@ DelegateChooser {
 	}
 	DelegateChoice {
 		roleValue: MtxEvent.Redacted
-		Redacted {}
+		Pill {
+			text: qsTr("redacted")
+		}
+	}
+	DelegateChoice {
+		roleValue: MtxEvent.Encryption
+		Pill {
+			text: qsTr("Encryption enabled")
+		}
 	}
 	DelegateChoice {
 		Placeholder {}
diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Pill.qml
similarity index 91%
rename from resources/qml/delegates/Redacted.qml
rename to resources/qml/delegates/Pill.qml
index 42fb4835..53a9684e 100644
--- a/resources/qml/delegates/Redacted.qml
+++ b/resources/qml/delegates/Pill.qml
@@ -2,7 +2,6 @@ import QtQuick 2.5
 import QtQuick.Controls 2.1
 
 Label {
-	text: qsTr("redacted")
 	color: inactiveColors.text
 	horizontalAlignment: Text.AlignHCenter
 
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 68b09f7b..1207ac77 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -20,7 +20,7 @@ Rectangle {
 		Rectangle {
 			id: videoContainer
 			visible: model.type == MtxEvent.VideoMessage
-			width: Math.min(parent.width, model.width)
+			width: Math.min(parent.width, model.width ? model.width : 400) // some media has 0 as size...
 			height: width*model.proportionalHeight
 			Image {
 				anchors.fill: parent
diff --git a/resources/res.qrc b/resources/res.qrc
index c9938d57..53406c48 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -128,7 +128,7 @@
         qml/delegates/ImageMessage.qml
         qml/delegates/PlayableMediaMessage.qml
         qml/delegates/FileMessage.qml
-        qml/delegates/Redacted.qml
+        qml/delegates/Pill.qml
         qml/delegates/Placeholder.qml
     
 
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 11344e60..b904dfd7 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -320,7 +320,10 @@ eventPropHeight(const mtx::events::RoomEvent &e)
         auto w = eventWidth(e);
         if (w == 0)
                 w = 1;
-        return eventHeight(e) / (double)w;
+
+        double prop = eventHeight(e) / (double)w;
+
+        return prop > 0 ? prop : 1.;
 }
 }