From 8e611abe87dd78095a35d156874a5b70ca72eb3e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 30 Aug 2019 19:29:25 +0200 Subject: Replace timeline with empty qml view --- src/ChatPage.cpp | 4 +- src/popups/UserMentions.cpp | 77 +++++++++++++++++++---------------- src/timeline2/TimelineViewManager.cpp | 10 +++++ src/timeline2/TimelineViewManager.h | 72 ++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 37 deletions(-) create mode 100644 src/timeline2/TimelineViewManager.cpp create mode 100644 src/timeline2/TimelineViewManager.h (limited to 'src') 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 -- cgit 1.5.1 From 8b5c7b2f2fb43b4f1884e683d60dd2553b9aa994 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 30 Aug 2019 23:20:53 +0200 Subject: 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 (limited to 'src') 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 -- cgit 1.5.1 From 47fbfd3f44154faf796c0be47dddfcba1b509a12 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 31 Aug 2019 22:43:31 +0200 Subject: 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(-) (limited to 'src') 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; }; -- cgit 1.5.1 From 699fd7b38e3c03a78cf54b0864ec63b25818b695 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 31 Aug 2019 23:44:17 +0200 Subject: 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(-) (limited to 'src') 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()); } -- cgit 1.5.1 From 2dd636456c22a85751e3484776c45efc9458b9ba Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 1 Sep 2019 14:17:33 +0200 Subject: 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 (limited to 'src') 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 75e03aeb..00000000 Binary files a/src/timeline/.TimelineItem.cpp.swp and /dev/null differ diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 10a5d3bf..8a74edaf 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -22,6 +22,13 @@ senderId(const T &event) { return QString::fromStdString(event.sender); } + +template +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_; } -- cgit 1.5.1 From ccedbde38b312f907c2845132ff60f57bcef7c08 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 1 Sep 2019 22:34:36 +0200 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From 56e27ced2555fe6d2ae4644c391cc81a9b5bfd85 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 1 Sep 2019 22:58:26 +0200 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From 34f5400e99eb0ecbf402fd5e7961dfed7b076ed2 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 2 Sep 2019 23:28:05 +0200 Subject: 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 (limited to 'src') 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); -- cgit 1.5.1 From c1ee22a53eaca688ce0975e8a0732ce193930945 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Sep 2019 02:14:49 +0200 Subject: Fix shadow warning --- src/timeline2/TimelineModel.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') 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); -- cgit 1.5.1 From c4ba832331e2daa17fd0efc724a2ccb0b68b18a9 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Sep 2019 08:23:07 +0200 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From bbbd5df75f97271506e366325a378f1623881f3d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Sep 2019 01:33:46 +0200 Subject: 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 (limited to 'src') 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())); }); } -- cgit 1.5.1 From 8727831de7308ee5cf202c9b20a7bbf916405b2a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Sep 2019 02:01:44 +0200 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From aae295cb02920d00dd6f31b82f9f267aa10f42de Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Sep 2019 14:37:54 +0200 Subject: Fix new messages not arriving in qml timeline --- src/timeline2/TimelineModel.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'src') 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(); } -- cgit 1.5.1 From ebeb1eb7721f357b016f6e914509918b6bee5356 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Sep 2019 22:22:07 +0200 Subject: 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 (limited to 'src') 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); -- cgit 1.5.1 From 86f4119a0502ffefd60abd5963f0d52628ba4e78 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Sep 2019 12:44:46 +0200 Subject: 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 (limited to 'src') 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; -- cgit 1.5.1 From 7aca8a94304b47904f214325b969c115944296c8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Sep 2019 15:26:46 +0200 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From e20501cec791d396ae03bc63958a36756607e711 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Sep 2019 16:50:32 +0200 Subject: Reenable display of encrypted messages --- src/timeline2/TimelineModel.cpp | 125 +++++++++++++++++++++++++++++++++++----- src/timeline2/TimelineModel.h | 15 ++++- 2 files changed, 125 insertions(+), 15 deletions(-) (limited to 'src') 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; -- cgit 1.5.1 From f260b8b4aede752d66e4351fe4c09282d7b93db7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Sep 2019 17:28:40 +0200 Subject: Fix shadow error --- src/timeline2/TimelineModel.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') 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) --", -- cgit 1.5.1 From 4efac5a247721c1c5ba4c072be0bce21ee620039 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Sep 2019 20:33:22 +0200 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From a1c97fc8d6e6f835aab79e2f8e37ce8488bcb5b6 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Sep 2019 21:42:33 +0200 Subject: Show redactions in qml timeline --- resources/qml/TimelineView.qml | 3 +- resources/qml/delegates/Redacted.qml | 15 ++++++++ resources/res.qrc | 1 + src/timeline2/TimelineModel.cpp | 75 ++++++++++++++++++++++++++---------- src/timeline2/TimelineModel.h | 3 ++ 5 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 resources/qml/delegates/Redacted.qml (limited to 'src') 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,26 +326,62 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) if (timeline.events.empty()) return; + std::vector ids = internalAddEvents(timeline.events); + + if (ids.empty()) + return; + + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size() + ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); + endInsertRows(); +} + +std::vector +TimelineModel::internalAddEvents( + const std::vector &timeline) +{ std::vector ids; - for (const auto &e : timeline.events) { + 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); } - - if (ids.empty) - return; - - beginInsertRows(QModelIndex(), - static_cast(this->eventOrder.size()), - static_cast(this->eventOrder.size() + ids.size() - 1)); - this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); - endInsertRows(); + return ids; } void @@ -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; -- cgit 1.5.1 From a7595eab5a6fdcde80b9989f65de1801afad3d3d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 11 Sep 2019 00:00:04 +0200 Subject: 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(-) (limited to 'src') 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, -- cgit 1.5.1 From 5c87d6faa60b14e4f84c67b2839615cbcb927f9f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 11 Sep 2019 00:17:45 +0200 Subject: 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(-) (limited to 'src') 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; -- cgit 1.5.1 From 62d0cd74da856028147ce222f3af9ad940b70a1b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 11 Sep 2019 00:54:40 +0200 Subject: 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(+) (limited to 'src') 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 -- cgit 1.5.1 From 691c8542019284044e50ef14e166930993953be8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 11 Sep 2019 00:59:04 +0200 Subject: Try to fix CI, no match for QString::arg(QStringRef) --- src/timeline2/TimelineViewManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') 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); -- cgit 1.5.1 From d1fffd66170d7548926b205dda7d8de81bef3384 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 18 Sep 2019 20:34:30 +0200 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From 240b3a566b8f73261bd6c48ae7480800136e3ec2 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 18 Sep 2019 22:58:25 +0200 Subject: 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 (limited to 'src') 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())); }); } -- cgit 1.5.1 From d34067a25792b5d69b2ce3192486189f0db12abb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 18 Sep 2019 23:37:30 +0200 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From 6c7e6b0e86ae78479b5f87b8440a8d10f99f14e0 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 19 Sep 2019 21:47:16 +0200 Subject: Fix read indicator --- resources/qml/StatusIndicator.qml | 4 ++-- src/timeline2/TimelineModel.cpp | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) (limited to 'src') 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) { -- cgit 1.5.1 From bb60976e7e9ca2a3f9ad54a571564de2b42c5d5c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 19 Sep 2019 22:44:25 +0200 Subject: Reenable encrypted messages --- src/timeline2/TimelineModel.cpp | 301 ++++++++++++++++++++++++++++++++++++++++ src/timeline2/TimelineModel.h | 57 +++++--- 2 files changed, 342 insertions(+), 16 deletions(-) (limited to 'src') 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())); + }); } -- cgit 1.5.1 From a5ccd00be0d58970a37944a68a91b7902cbf450f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 19 Sep 2019 22:45:51 +0200 Subject: Remove noisy decrypted message --- src/timeline2/TimelineModel.cpp | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') 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); -- cgit 1.5.1 From 82091999c4fc81412c726d28a339b305709bacd0 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 19 Sep 2019 23:02:56 +0200 Subject: 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 (limited to 'src') 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; -- cgit 1.5.1 From 9b18440b4f989da4fcd764f33291d17dbbcb82e3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 28 Sep 2019 11:07:58 +0200 Subject: 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(-) (limited to 'src') 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; }; -- cgit 1.5.1 From e2d733a01a1c936d22ec6c67b2f3b57ac0afdabb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 29 Sep 2019 10:45:35 +0200 Subject: 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 | 98 ++++++++++++++++++++++++++------ src/timeline2/TimelineViewManager.h | 25 +++++++- 9 files changed, 174 insertions(+), 33 deletions(-) (limited to 'src') 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()); - - auto imgDialog = new dialogs::ImageOverlay(pixmap); - imgDialog->show(); - // connect(imgDialog, &dialogs::ImageOverlay::saving, this, - // &ImageItem::saveAs); - Q_UNUSED(imgDialog); - }); + 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 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); -- cgit 1.5.1 From 0d3c9390c67d4f0fefdfa192009291ff024ecce8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 29 Sep 2019 11:28:55 +0200 Subject: 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(-) (limited to 'src') 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_; } -- cgit 1.5.1 From aee29c6ed5b9cc9b60714cfb2f744109651ac035 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 29 Sep 2019 12:29:17 +0200 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From 1dd1a19b06861e2ab0fc282af143e792080bbbdb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 3 Oct 2019 18:07:01 +0200 Subject: 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(-) (limited to 'src') 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: -- cgit 1.5.1 From ea12c9f9bc9918884da8f7a930484412b93f9426 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 3 Oct 2019 22:39:56 +0200 Subject: Add basic read_event support (qml) --- src/timeline2/TimelineModel.cpp | 21 +++++++++++++++++++++ src/timeline2/TimelineModel.h | 6 +----- 2 files changed, 22 insertions(+), 5 deletions(-) (limited to 'src') 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); -- cgit 1.5.1 From a8166462adc8ffd8d6c5d2a9a50e5cde5c810588 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 4 Oct 2019 01:10:46 +0200 Subject: 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 (limited to 'src') 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, -- cgit 1.5.1 From ea98d7b2aed5d5085d0ce1833568ec93ff813b0f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 5 Oct 2019 23:11:20 +0200 Subject: 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 (limited to 'src') 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); -- cgit 1.5.1 From 425d534e225a956fefb3bc2e8d2c8d26152f6353 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 01:44:02 +0200 Subject: 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(-) (limited to 'src') 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(); } -- cgit 1.5.1 From 241c0236fc2b98033bb4d9468cec82c21016b009 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 12:59:51 +0200 Subject: Try to fix windows Winsock.h compilation error --- src/timeline2/TimelineModel.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') 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" -- cgit 1.5.1 From ec6953d0c09292b223147c299f893325e50fbf3b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 14:00:49 +0200 Subject: Fix linting issues --- src/timeline2/TimelineModel.cpp | 2 +- src/timeline2/TimelineModel.h | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'src') 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 -- cgit 1.5.1 From 8ebef4eed2134179e5609104eb72fe8f055a35f1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 9 Oct 2019 00:36:03 +0200 Subject: Size images/videos by timeline width --- resources/qml/delegates/ImageMessage.qml | 4 ++-- resources/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(-) (limited to 'src') 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, -- cgit 1.5.1 From 0fd2199112be88a17125ea5630ce6184eb0758ca Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 9 Oct 2019 19:34:57 +0200 Subject: Load content if no scrollbar is needed --- resources/qml/TimelineView.qml | 10 ++++++++++ src/timeline2/TimelineViewManager.cpp | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) (limited to 'src') 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()); } -- cgit 1.5.1 From a83ae7e95fcb21f0f61cdb7d8cf9e4c4985bd853 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 13 Oct 2019 15:10:33 +0200 Subject: 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(-) (limited to 'src') 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 -- cgit 1.5.1 From cff46d97a8636f41dd5ac2ace9dc00ecb5f4c51c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 17 Oct 2019 09:36:16 +0200 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From c37495fae29cd93b1b03e4e3689c604cc5312a18 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 20 Oct 2019 12:39:47 +0200 Subject: 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 (limited to 'src') 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); -- cgit 1.5.1 From c8f97216faa74b9d3f1f92af7acc509dbe6b5647 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 25 Oct 2019 11:46:25 +0200 Subject: 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(-) (limited to 'src') 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; -- cgit 1.5.1 From 3d6f502bcc4bae477eb3f8d51aa7b90a6c9e9f46 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 25 Oct 2019 13:20:05 +0200 Subject: 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(-) (limited to 'src') 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 *); -- cgit 1.5.1 From 0d8bf6c67693cc1e41bd9fab7ba7924088506b84 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 25 Oct 2019 14:20:43 +0200 Subject: lint --- src/timeline2/DelegateChooser.cpp | 2 +- src/timeline2/DelegateChooser.h | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) (limited to 'src') 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 *); }; - -- cgit 1.5.1 From b1f1cb2b560aa56d485ba1e326bf111326c7aa74 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 27 Oct 2019 22:49:49 +0100 Subject: Redirect qt logger --- src/Logging.cpp | 39 +++++++++++++++++++++++++++++++++++++++ src/Logging.h | 3 +++ 2 files changed, 42 insertions(+) (limited to 'src') 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; } -- cgit 1.5.1 From 15badebc7735d554a39cf0eb50e400ee95c1e0c8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 28 Oct 2019 20:39:02 +0100 Subject: Show own messages in RoomList --- src/timeline2/TimelineModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') 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)) { -- cgit 1.5.1 From 6b6085b270bcdffe56e19de1cd1171a73fe5fba1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 31 Oct 2019 14:09:51 +0100 Subject: 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(-) (limited to 'src') 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)); -- cgit 1.5.1 From 2c37beba8dc5b32e843010ba117abb84951c820b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 2 Nov 2019 18:17:06 +0100 Subject: 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(-) (limited to 'src') 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. -- cgit 1.5.1 From bde71a6cbcce17005c464d3cb77a8d8e5d3a8566 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 2 Nov 2019 21:14:22 +0100 Subject: fixup bad room list translation commit --- src/Cache.h | 1 - src/Utils.h | 3 --- 2 files changed, 4 deletions(-) (limited to 'src') 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); -- cgit 1.5.1 From 4f7a45a0a6b28255fb4c1ccbb53fcf8a6958843a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 3 Nov 2019 01:17:40 +0100 Subject: 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(-) (limited to 'src') 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(); -- cgit 1.5.1 From 993926e189e213e9bb809c458cf5599d3aea055d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 3 Nov 2019 02:09:36 +0100 Subject: 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 (limited to 'src') 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); -- cgit 1.5.1 From 88dc72df4f7cd6cabdb48866e6030f5e506eb24f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 3 Nov 2019 03:28:16 +0100 Subject: 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 (limited to 'src') 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; -- cgit 1.5.1 From 1268e9f11c22e8cd22302342e80daef94b15001d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 5 Nov 2019 17:16:04 +0100 Subject: 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(-) (limited to 'src') 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; -- cgit 1.5.1 From 2bfb885b4739dae46a35cfc5b1c62767b3900da9 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 8 Nov 2019 14:39:45 +0100 Subject: 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 | 238 +++++++++++++++++----------------- 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, 194 insertions(+), 149 deletions(-) create mode 100644 src/ColorImageProvider.cpp create mode 100644 src/ColorImageProvider.h (limited to 'src') 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 - } + Rectangle { + anchors.fill: parent + color: colors.window + + Text { + visible: !timelineManager.timeline + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + color: colors.windowText + } - ListView { - id: chat + ListView { + id: chat - cacheBuffer: 2000 + cacheBuffer: 2000 - visible: timelineManager.timeline != null - anchors.fill: parent + visible: timelineManager.timeline != null + anchors.fill: parent - anchors.leftMargin: 4 - anchors.rightMargin: scrollbar.width + anchors.leftMargin: 4 + anchors.rightMargin: scrollbar.width - model: timelineManager.timeline + model: timelineManager.timeline - onModelChanged: { - if (model) { - currentIndex = model.currentIndex - if (model.currentIndex == count - 1) { - positionViewAtEnd() - } else { - positionViewAtIndex(model.currentIndex, ListView.End) - } + onModelChanged: { + if (model) { + currentIndex = model.currentIndex + if (model.currentIndex == count - 1) { + positionViewAtEnd() + } else { + positionViewAtIndex(model.currentIndex, ListView.End) + } - if (contentHeight < height) { - model.fetchHistory(); + //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 && Window.active) { - var newIndex = count - 1 // last index - positionViewAtEnd() - currentIndex = newIndex - model.currentIndex = newIndex + ScrollBar.vertical: ScrollBar { + id: scrollbar + anchors.top: parent.top + anchors.left: parent.right + anchors.bottom: parent.bottom + onPressedChanged: if (!pressed) chat.updatePosition() } - if (contentHeight < height && model) { - model.fetchHistory(); - } - } + property bool atBottom: false + onCountChanged: { + if (atBottom) { + var newIndex = count - 1 // last index + positionViewAtEnd() + currentIndex = newIndex + model.currentIndex = newIndex + } - 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; + if (contentHeight < height && model) { + model.fetchHistory(); + } } - } - onMovementEnded: updatePosition() - spacing: 4 - delegate: TimelineRow { - function isFullyVisible() { - return height > 1 && (y - chat.contentY - 1) + height < chat.height - } - function getIndex() { - return index; + onAtYBeginningChanged: if (atYBeginning) model.fetchHistory() + + 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; + } } - } + onMovementEnded: updatePosition() - 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 - } + spacing: 4 + delegate: TimelineRow { + function isFullyVisible() { + return height > 1 && (y - chat.contentY - 1) + height < chat.height } - 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]) - - MouseArea { - anchors.fill: parent - onClicked: chat.model.openUserProfile(section.split(" ")[0]) - cursorShape: Qt.PointingHandCursor + 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]) + + 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 + 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; }; -- cgit 1.5.1 From 91d1f19058a31cc35ca1212f042a9dd6f501a7b7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 9 Nov 2019 03:06:10 +0100 Subject: Remove old timeline --- CMakeLists.txt | 26 +- src/ChatPage.cpp | 2 +- src/Utils.h | 14 +- src/dialogs/MemberList.cpp | 1 + src/timeline/DelegateChooser.cpp | 138 +++ src/timeline/DelegateChooser.h | 82 ++ src/timeline/TimelineItem.cpp | 960 ------------------- src/timeline/TimelineItem.h | 389 -------- src/timeline/TimelineModel.cpp | 1220 ++++++++++++++++++++++++ src/timeline/TimelineModel.h | 258 ++++++ src/timeline/TimelineView.cpp | 1627 --------------------------------- src/timeline/TimelineView.h | 449 --------- src/timeline/TimelineViewManager.cpp | 564 +++++++----- src/timeline/TimelineViewManager.h | 111 ++- 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/DelegateChooser.cpp | 138 --- src/timeline2/DelegateChooser.h | 82 -- src/timeline2/TimelineModel.cpp | 1220 ------------------------ src/timeline2/TimelineModel.h | 258 ------ src/timeline2/TimelineViewManager.cpp | 400 -------- src/timeline2/TimelineViewManager.h | 117 --- 28 files changed, 2088 insertions(+), 7095 deletions(-) create mode 100644 src/timeline/DelegateChooser.cpp create mode 100644 src/timeline/DelegateChooser.h delete mode 100644 src/timeline/TimelineItem.cpp delete mode 100644 src/timeline/TimelineItem.h create mode 100644 src/timeline/TimelineModel.cpp create mode 100644 src/timeline/TimelineModel.h 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/DelegateChooser.cpp delete mode 100644 src/timeline2/DelegateChooser.h delete mode 100644 src/timeline2/TimelineModel.cpp delete mode 100644 src/timeline2/TimelineModel.h delete mode 100644 src/timeline2/TimelineViewManager.cpp delete mode 100644 src/timeline2/TimelineViewManager.h (limited to 'src') 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/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp new file mode 100644 index 00000000..632a2a64 --- /dev/null +++ b/src/timeline/DelegateChooser.cpp @@ -0,0 +1,138 @@ +#include "DelegateChooser.h" + +#include "Logging.h" + +// uses private API, which moved between versions +#include +#include + +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); +} + +void +DelegateChooser::appendChoice(QQmlListProperty *p, DelegateChoice *c) +{ + DelegateChooser *dc = static_cast(p->object); + dc->choices_.append(c); +} + +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_) { + if (child) { + child->setParentItem(nullptr); + child = nullptr; + } + + choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); + return; + } + } +} + +void +DelegateChooser::componentComplete() +{ + QQuickItem::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!"); + 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/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h new file mode 100644 index 00000000..68ebeb04 --- /dev/null +++ b/src/timeline/DelegateChooser.h @@ -0,0 +1,82 @@ +// 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 +#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(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + + QQmlListProperty choices(); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + + void recalcChild(); + void componentComplete() override; + +signals: + void roleChanged(); + 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 = nullptr; + DelegateIncubator incubator{*this}; + + static void appendChoice(QQmlListProperty *, DelegateChoice *); + static int choiceCount(QQmlListProperty *); + static DelegateChoice *choice(QQmlListProperty *, int index); + static void clearChoices(QQmlListProperty *); +}; 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/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp new file mode 100644 index 00000000..ab7d3d47 --- /dev/null +++ b/src/timeline/TimelineModel.cpp @@ -0,0 +1,1220 @@ +#include "TimelineModel.h" + +#include +#include + +#include + +#include "ChatPage.h" +#include "Logging.h" +#include "MainWindow.h" +#include "Olm.h" +#include "TimelineViewManager.h" +#include "Utils.h" +#include "dialogs/RawMessage.h" + +Q_DECLARE_METATYPE(QModelIndex) + +namespace { +template +QString +eventId(const mtx::events::RoomEvent &event) +{ + return QString::fromStdString(event.event_id); +} +template +QString +roomId(const mtx::events::Event &event) +{ + return QString::fromStdString(event.room_id); +} +template +QString +senderId(const mtx::events::RoomEvent &event) +{ + return QString::fromStdString(event.sender); +} + +template +QDateTime +eventTimestamp(const mtx::events::RoomEvent &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 &) +{ + 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()) { + return QString::fromStdString(temp); + } else { + return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "
"); + } +} + +template +QString +eventUrl(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventUrl(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + 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 &) +{ + 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 +auto +eventFilesize(const mtx::events::RoomEvent &e) -> decltype(e.content.info.size) +{ + return e.content.info.size; +} + +template +int64_t +eventFilesize(const mtx::events::Event &) +{ + return 0; +} + +template +QString +eventMimeType(const mtx::events::Event &) +{ + return QString(); +} +template +auto +eventMimeType(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + 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) +{ + 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; +} + +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::Redacted; +} +// ::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(TimelineViewManager *manager, QString room_id, QObject *parent) + : QAbstractListModel(parent) + , room_id_(room_id) + , manager_(manager) +{ + 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); + + // mark our messages as read + readEvent(event_id.toStdString()); + + // ask to be notified for read receipts + cache::client()->addPendingReceipt(room_id_, event_id); + + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { + emit ChatPage::instance()->showNotification(msg); + }); +} + +QHash +TimelineModel::roleNames() const +{ + return { + {Section, "section"}, + {Type, "type"}, + {Body, "body"}, + {FormattedBody, "formattedBody"}, + {UserId, "userId"}, + {UserName, "userName"}, + {Timestamp, "timestamp"}, + {Url, "url"}, + {ThumbnailUrl, "thumbnailUrl"}, + {Filename, "filename"}, + {Filesize, "filesize"}, + {MimeType, "mimetype"}, + {Height, "height"}, + {Width, "width"}, + {ProportionalHeight, "proportionalHeight"}, + {Id, "id"}, + {State, "state"}, + {IsEncrypted, "isEncrypted"}, + {ReplyTo, "replyTo"}, + }; +} +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()]; + + 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); }, event); + date.setTime(QTime()); + + 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( + [](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); }, event)); + case UserName: + return QVariant(displayName(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, event))); + + case Timestamp: + return QVariant(boost::apply_visitor( + [](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); }, + 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)) + .remove("") + .remove("")); + 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)); + 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)); + case Height: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventHeight(e); }, event)); + case Width: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventWidth(e); }, event)); + case ProportionalHeight: + return QVariant(boost::apply_visitor( + [](const auto &e) -> double { return eventPropHeight(e); }, event)); + case Id: + return id; + case State: + // 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) || + cache::client()->readReceipts(id, room_id_).size() > 1) + return qml_mtx_events::Read; + else + return qml_mtx_events::Received; + case IsEncrypted: { + auto tempEvent = events[id]; + 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(); + } +} + +void +TimelineModel::addEvents(const mtx::responses::Timeline &timeline) +{ + if (isInitialSync) { + prev_batch_token_ = QString::fromStdString(timeline.prev_batch); + isInitialSync = false; + } + + if (timeline.events.empty()) + return; + + std::vector ids = internalAddEvents(timeline.events); + + if (ids.empty()) + return; + + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size() + ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); + endInsertRows(); + + updateLastMessage(); +} + +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 +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)) { + this->events.insert(id, e); + int idx = idToIndex(id); + emit dataChanged(index(idx, 0), index(idx, 0)); + 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() +{ + if (paginationInProgress) { + nhlog::ui()->warn("Already loading older messages"); + return; + } + + paginationInProgress = true; + mtx::http::MessagesOpts opts; + opts.room_id = room_id_.toStdString(); + opts.from = prev_batch_token_.toStdString(); + + nhlog::ui()->info("Paginationg room {}", opts.room_id); + + http::client()->messages( + opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("failed to call /messages ({}): {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + paginationInProgress = false; + return; + } + + emit oldMessagesRetrieved(std::move(res)); + paginationInProgress = false; + }); +} + +void +TimelineModel::setCurrentIndex(int index) +{ + auto oldIndex = idToIndex(currentId); + currentId = indexToId(index); + emit currentIndexChanged(index); + + 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) +{ + std::vector ids = internalAddEvents(msgs.chunk); + + if (!ids.empty()) { + beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); + endInsertRows(); + } + + prev_batch_token_ = QString::fromStdString(msgs.end); +} + +QColor +TimelineModel::userColor(QString id, QColor background) +{ + if (!userColors.contains(id)) + userColors.insert( + id, QColor(utils::generateContrastingHexColor(id, background.name()))); + return userColors.value(id); +} + +QString +TimelineModel::displayName(QString id) const +{ + return Cache::displayName(room_id_, id); +} + +QString +TimelineModel::avatarUrl(QString id) const +{ + return Cache::avatarUrl(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); +} + +QString +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); +} + +void + +TimelineModel::openUserProfile(QString userid) const +{ + MainWindow::instance()->openUserProfile(userid, room_id_); +} + +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; + + json event_array = json::array(); + event_array.push_back(body); + + std::vector temp_events; + mtx::responses::utils::parse_timeline_events(event_array, temp_events); + + if (temp_events.size() == 1) + return {temp_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 +TimelineModel::replyAction(QString id) +{ + auto event = events.value(id); + RelatedInfo related = boost::apply_visitor( + [](const auto &ev) -> 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 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()) + return; + + ChatPage::instance()->messageReply(related); +} + +void +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 +{ + 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]; +} + +// Note: this will only be called for our messages +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)); + } +} + +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/timeline/TimelineModel.h b/src/timeline/TimelineModel.h new file mode 100644 index 00000000..31e41315 --- /dev/null +++ b/src/timeline/TimelineModel.h @@ -0,0 +1,258 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" + +namespace qml_mtx_events { +Q_NAMESPACE + +enum EventType +{ + // Unsupported event + Unsupported, + /// m.room_key_request + KeyRequest, + /// m.room.aliases + Aliases, + /// m.room.avatar + Avatar, + /// m.room.canonical_alias + CanonicalAlias, + /// m.room.create + Create, + /// m.room.encrypted. + Encrypted, + /// m.room.encryption. + Encryption, + /// m.room.guest_access + GuestAccess, + /// m.room.history_visibility + HistoryVisibility, + /// m.room.join_rules + JoinRules, + /// m.room.member + Member, + /// m.room.name + Name, + /// m.room.power_levels + PowerLevels, + /// m.room.tombstone + Tombstone, + /// m.room.topic + Topic, + /// m.room.redaction + Redaction, + /// m.room.pinned_events + PinnedEvents, + // m.sticker + Sticker, + // m.tag + Tag, + /// m.room.message + AudioMessage, + EmoteMessage, + FileMessage, + ImageMessage, + LocationMessage, + NoticeMessage, + TextMessage, + VideoMessage, + Redacted, + UnknownMessage, +}; +Q_ENUM_NS(EventType) + +enum EventState +{ + //! The plaintext message was received by the server. + Received, + //! At least one of the participants has read the message. + Read, + //! The client sent the message. Not yet received. + Sent, + //! When the message is loaded from cache or backfill. + Empty, + //! When the message failed to send + Failed, +}; +Q_ENUM_NS(EventState) +} + +class StateKeeper +{ +public: + StateKeeper(std::function &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function fn_; +}; + +struct DecryptionResult +{ + //! The decrypted content as a normal plaintext event. + mtx::events::collections::TimelineEvents event; + //! Whether or not the decryption was successful. + bool isDecrypted = false; +}; + +class TimelineViewManager; + +class TimelineModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY( + int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + +public: + explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0); + + enum Roles + { + Section, + Type, + Body, + FormattedBody, + UserId, + UserName, + Timestamp, + Url, + ThumbnailUrl, + Filename, + Filesize, + MimeType, + Height, + Width, + ProportionalHeight, + Id, + State, + IsEncrypted, + ReplyTo, + }; + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE QColor userColor(QString id, QColor background); + Q_INVOKABLE QString displayName(QString id) const; + Q_INVOKABLE QString avatarUrl(QString id) const; + Q_INVOKABLE QString formatDateSeparator(QDate date) const; + + Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void openUserProfile(QString userid) const; + Q_INVOKABLE void replyAction(QString id); + Q_INVOKABLE void readReceiptsAction(QString id) const; + Q_INVOKABLE void redactEvent(QString id); + Q_INVOKABLE int idToIndex(QString id) const; + Q_INVOKABLE QString indexToId(int index) const; + + void addEvents(const mtx::responses::Timeline &events); + template + void sendMessage(const T &msg); + +public slots: + void fetchHistory(); + void setCurrentIndex(int index); + int currentIndex() const { return idToIndex(currentId); } + void markEventsAsRead(const std::vector &event_ids); + +private slots: + // Add old events at the top of the timeline. + void addBackwardsEvents(const mtx::responses::Messages &msgs); + +signals: + void oldMessagesRetrieved(const mtx::responses::Messages &res); + void messageFailed(QString txn_id); + void messageSent(QString txn_id, QString event_id); + void currentIndexChanged(int index); + void redactionFailed(QString id); + void eventRedacted(QString id); + +private: + DecryptionResult decryptEvent( + const mtx::events::EncryptedEvent &e) const; + std::vector internalAddEvents( + const std::vector &timeline); + void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content); + void handleClaimedKeys(std::shared_ptr keeper, + const std::map &room_key, + const std::map &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err); + void updateLastMessage(); + void readEvent(const std::string &id); + + QHash events; + QSet pending, failed, read; + std::vector eventOrder; + + QString room_id_; + QString prev_batch_token_; + + bool isInitialSync = true; + bool paginationInProgress = false; + + QHash userColors; + QString currentId; + + TimelineViewManager *manager_; +}; + +template +void +TimelineModel::sendMessage(const T &msg) +{ + auto txn_id = http::client()->generate_txn_id(); + mtx::events::RoomEvent msgCopy = {}; + msgCopy.content = msg; + msgCopy.type = mtx::events::EventType::RoomMessage; + msgCopy.event_id = txn_id; + msgCopy.sender = http::client()->user_id().to_string(); + msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + internalAddEvents({msgCopy}); + + QString txn_id_qstr = QString::fromStdString(txn_id); + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size())); + pending.insert(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); + endInsertRows(); + updateLastMessage(); + + if (cache::client()->isRoomEncrypted(room_id_.toStdString())) + sendEncryptedMessage(txn_id, nlohmann::json(msg)); + else + http::client()->send_room_message( + room_id_.toStdString(), + txn_id, + msg, + [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = static_cast(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(txn_id_qstr); + } + emit messageSent(txn_id_qstr, + QString::fromStdString(res.event_id.to_string())); + }); +} 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,340 +1,400 @@ -/* - * 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 "Cache.h" +#include "TimelineViewManager.h" + +#include +#include +#include +#include +#include +#include + +#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" - -TimelineViewManager::TimelineViewManager(QWidget *parent) - : QStackedWidget(parent) -{} +#include "MxcImageProvider.h" +#include "UserSettingsPage.h" +#include "dialogs/ImageOverlay.h" void -TimelineViewManager::updateReadReceipts(const QString &room_id, - const std::vector &event_ids) +TimelineViewManager::updateColorPalette() { - if (timelineViewExists(room_id)) { - auto view = views_[room_id]; - if (view) - emit view->markReadEvents(event_ids); + 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); } } -void -TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) +TimelineViewManager::TimelineViewManager(QWidget *parent) + : imgProvider(new MxcImageProvider()) + , colorImgProvider(new ColorImageProvider()) { - auto view = views_[room_id]; - - if (view) - view->removeEvent(event_id); + 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::queueTextMessage(const QString &msg) +TimelineViewManager::sync(const mtx::responses::Rooms &rooms) { - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Text, msg); + 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::queueEmoteMessage(const QString &msg) +TimelineViewManager::addRoom(const QString &room_id) { - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Emote, msg); + if (!models.contains(room_id)) + models.insert(room_id, + QSharedPointer(new TimelineModel(this, room_id))); } void -TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) +TimelineViewManager::setHistoryView(const QString &room_id) { - if (active_room_.isEmpty()) - return; + nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Text, reply, related); + 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::queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size, - const QSize &dimensions) +TimelineViewManager::openImageOverlay(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const { - 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); + 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::queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::saveMedia(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.file message to a non-managed view"); - return; + 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"); } - auto view = views_[roomid]; + QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + + auto filename = + QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString); + + if (filename.isEmpty()) + return; - view->addUserMessage(url, filename, mime, size); + 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::queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.audio message to a non-managed view"); + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); return; } - auto view = views_[roomid]; - - view->addUserMessage(url, filename, mime, size); -} + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); -void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) -{ - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.video message to a non-managed view"); + 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; } - auto view = views_[roomid]; + QDir().mkpath(filename.path()); - view->addUserMessage(url, filename, mime, size); + 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::initialize(const mtx::responses::Rooms &rooms) +TimelineViewManager::updateReadReceipts(const QString &room_id, + const std::vector &event_ids) { - for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { - addRoom(it->second, QString::fromStdString(it->first)); + auto room = models.find(room_id); + if (room != models.end()) { + room.value()->markEventsAsRead(event_ids); } - - 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)); + for (const auto &e : msgs) { + addRoom(e.first); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(view); + models.value(e.first)->addEvents(e.second); } } void -TimelineViewManager::initialize(const std::vector &rooms) +TimelineViewManager::queueTextMessage(const QString &msg) { - for (const auto &roomid : rooms) - addRoom(QString::fromStdString(roomid)); + 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::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id) +TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) { - 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)); + 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); + } + } - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + 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; - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(text); } void -TimelineViewManager::addRoom(const QString &room_id) +TimelineViewManager::queueEmoteMessage(const QString &msg) { - if (timelineViewExists(room_id)) - return; + auto html = utils::markdownToHtml(msg); - // Create a history view without any events. - TimelineView *view = new TimelineView(room_id); - views_.emplace(room_id, QSharedPointer(view)); + mtx::events::msg::Emote emote; + emote.body = msg.trimmed().toStdString(); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + if (html != msg.trimmed().toHtmlEscaped()) + emote.formatted_body = html.toStdString(); - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(emote); } void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) +TimelineViewManager::queueImageMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions) { - 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); - } + 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::setHistoryView(const QString &room_id) +TimelineViewManager::queueFileMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) { - 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(); + 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); } -QString -TimelineViewManager::chooseRandomColor() +void +TimelineViewManager::queueAudioMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) { - 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(); + 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); } -bool -TimelineViewManager::hasLoaded() const +void +TimelineViewManager::queueVideoMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) { - 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 -public: - TimelineViewManager(QWidget *parent); - - // Initialize with timeline events. - void initialize(const mtx::responses::Rooms &rooms); - // Empty initialization. - void initialize(const std::vector &rooms); + Q_PROPERTY( + TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) - void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id); - void addRoom(const QString &room_id); +public: + TimelineViewManager(QWidget *parent = 0); + QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); - void clearAll() { views_.clear(); } - - // Check if all the timelines have been loaded. - bool hasLoaded() const; + void addRoom(const QString &room_id); - static QString chooseRandomColor(); + 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(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(); } - - QString active_room_; - std::map> views_; +#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 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/DelegateChooser.cpp b/src/timeline2/DelegateChooser.cpp deleted file mode 100644 index 632a2a64..00000000 --- a/src/timeline2/DelegateChooser.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include "DelegateChooser.h" - -#include "Logging.h" - -// uses private API, which moved between versions -#include -#include - -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); -} - -void -DelegateChooser::appendChoice(QQmlListProperty *p, DelegateChoice *c) -{ - DelegateChooser *dc = static_cast(p->object); - dc->choices_.append(c); -} - -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_) { - if (child) { - child->setParentItem(nullptr); - child = nullptr; - } - - choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); - return; - } - } -} - -void -DelegateChooser::componentComplete() -{ - QQuickItem::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!"); - 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 deleted file mode 100644 index 68ebeb04..00000000 --- a/src/timeline2/DelegateChooser.h +++ /dev/null @@ -1,82 +0,0 @@ -// 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 -#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(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) - - QQmlListProperty choices(); - - QVariant roleValue() const; - void setRoleValue(const QVariant &value); - - void recalcChild(); - void componentComplete() override; - -signals: - void roleChanged(); - 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 = nullptr; - DelegateIncubator incubator{*this}; - - static void appendChoice(QQmlListProperty *, DelegateChoice *); - static int choiceCount(QQmlListProperty *); - static DelegateChoice *choice(QQmlListProperty *, int index); - static void clearChoices(QQmlListProperty *); -}; diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp deleted file mode 100644 index ab7d3d47..00000000 --- a/src/timeline2/TimelineModel.cpp +++ /dev/null @@ -1,1220 +0,0 @@ -#include "TimelineModel.h" - -#include -#include - -#include - -#include "ChatPage.h" -#include "Logging.h" -#include "MainWindow.h" -#include "Olm.h" -#include "TimelineViewManager.h" -#include "Utils.h" -#include "dialogs/RawMessage.h" - -Q_DECLARE_METATYPE(QModelIndex) - -namespace { -template -QString -eventId(const mtx::events::RoomEvent &event) -{ - return QString::fromStdString(event.event_id); -} -template -QString -roomId(const mtx::events::Event &event) -{ - return QString::fromStdString(event.room_id); -} -template -QString -senderId(const mtx::events::RoomEvent &event) -{ - return QString::fromStdString(event.sender); -} - -template -QDateTime -eventTimestamp(const mtx::events::RoomEvent &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 &) -{ - 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()) { - return QString::fromStdString(temp); - } else { - return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "
"); - } -} - -template -QString -eventUrl(const mtx::events::Event &) -{ - return ""; -} -template -auto -eventUrl(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, QString> -{ - 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 &) -{ - 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 -auto -eventFilesize(const mtx::events::RoomEvent &e) -> decltype(e.content.info.size) -{ - return e.content.info.size; -} - -template -int64_t -eventFilesize(const mtx::events::Event &) -{ - return 0; -} - -template -QString -eventMimeType(const mtx::events::Event &) -{ - return QString(); -} -template -auto -eventMimeType(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, QString> -{ - 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) -{ - 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; -} - -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::Redacted; -} -// ::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(TimelineViewManager *manager, QString room_id, QObject *parent) - : QAbstractListModel(parent) - , room_id_(room_id) - , manager_(manager) -{ - 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); - - // mark our messages as read - readEvent(event_id.toStdString()); - - // ask to be notified for read receipts - cache::client()->addPendingReceipt(room_id_, event_id); - - emit dataChanged(index(idx, 0), index(idx, 0)); - }); - connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { - emit ChatPage::instance()->showNotification(msg); - }); -} - -QHash -TimelineModel::roleNames() const -{ - return { - {Section, "section"}, - {Type, "type"}, - {Body, "body"}, - {FormattedBody, "formattedBody"}, - {UserId, "userId"}, - {UserName, "userName"}, - {Timestamp, "timestamp"}, - {Url, "url"}, - {ThumbnailUrl, "thumbnailUrl"}, - {Filename, "filename"}, - {Filesize, "filesize"}, - {MimeType, "mimetype"}, - {Height, "height"}, - {Width, "width"}, - {ProportionalHeight, "proportionalHeight"}, - {Id, "id"}, - {State, "state"}, - {IsEncrypted, "isEncrypted"}, - {ReplyTo, "replyTo"}, - }; -} -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()]; - - 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); }, event); - date.setTime(QTime()); - - 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( - [](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); }, event)); - case UserName: - return QVariant(displayName(boost::apply_visitor( - [](const auto &e) -> QString { return senderId(e); }, event))); - - case Timestamp: - return QVariant(boost::apply_visitor( - [](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); }, - 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)) - .remove("") - .remove("")); - 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)); - 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)); - case Height: - return QVariant(boost::apply_visitor( - [](const auto &e) -> qulonglong { return eventHeight(e); }, event)); - case Width: - return QVariant(boost::apply_visitor( - [](const auto &e) -> qulonglong { return eventWidth(e); }, event)); - case ProportionalHeight: - return QVariant(boost::apply_visitor( - [](const auto &e) -> double { return eventPropHeight(e); }, event)); - case Id: - return id; - case State: - // 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) || - cache::client()->readReceipts(id, room_id_).size() > 1) - return qml_mtx_events::Read; - else - return qml_mtx_events::Received; - case IsEncrypted: { - auto tempEvent = events[id]; - 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(); - } -} - -void -TimelineModel::addEvents(const mtx::responses::Timeline &timeline) -{ - if (isInitialSync) { - prev_batch_token_ = QString::fromStdString(timeline.prev_batch); - isInitialSync = false; - } - - if (timeline.events.empty()) - return; - - std::vector ids = internalAddEvents(timeline.events); - - if (ids.empty()) - return; - - beginInsertRows(QModelIndex(), - static_cast(this->eventOrder.size()), - static_cast(this->eventOrder.size() + ids.size() - 1)); - this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); - endInsertRows(); - - updateLastMessage(); -} - -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 -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)) { - this->events.insert(id, e); - int idx = idToIndex(id); - emit dataChanged(index(idx, 0), index(idx, 0)); - 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() -{ - if (paginationInProgress) { - nhlog::ui()->warn("Already loading older messages"); - return; - } - - paginationInProgress = true; - mtx::http::MessagesOpts opts; - opts.room_id = room_id_.toStdString(); - opts.from = prev_batch_token_.toStdString(); - - nhlog::ui()->info("Paginationg room {}", opts.room_id); - - http::client()->messages( - opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("failed to call /messages ({}): {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - paginationInProgress = false; - return; - } - - emit oldMessagesRetrieved(std::move(res)); - paginationInProgress = false; - }); -} - -void -TimelineModel::setCurrentIndex(int index) -{ - auto oldIndex = idToIndex(currentId); - currentId = indexToId(index); - emit currentIndexChanged(index); - - 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) -{ - std::vector ids = internalAddEvents(msgs.chunk); - - if (!ids.empty()) { - beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); - this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); - endInsertRows(); - } - - prev_batch_token_ = QString::fromStdString(msgs.end); -} - -QColor -TimelineModel::userColor(QString id, QColor background) -{ - if (!userColors.contains(id)) - userColors.insert( - id, QColor(utils::generateContrastingHexColor(id, background.name()))); - return userColors.value(id); -} - -QString -TimelineModel::displayName(QString id) const -{ - return Cache::displayName(room_id_, id); -} - -QString -TimelineModel::avatarUrl(QString id) const -{ - return Cache::avatarUrl(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); -} - -QString -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); -} - -void - -TimelineModel::openUserProfile(QString userid) const -{ - MainWindow::instance()->openUserProfile(userid, room_id_); -} - -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; - - json event_array = json::array(); - event_array.push_back(body); - - std::vector temp_events; - mtx::responses::utils::parse_timeline_events(event_array, temp_events); - - if (temp_events.size() == 1) - return {temp_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 -TimelineModel::replyAction(QString id) -{ - auto event = events.value(id); - RelatedInfo related = boost::apply_visitor( - [](const auto &ev) -> 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 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()) - return; - - ChatPage::instance()->messageReply(related); -} - -void -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 -{ - 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]; -} - -// Note: this will only be called for our messages -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)); - } -} - -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 deleted file mode 100644 index 31e41315..00000000 --- a/src/timeline2/TimelineModel.h +++ /dev/null @@ -1,258 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include - -#include "Cache.h" -#include "Logging.h" -#include "MatrixClient.h" - -namespace qml_mtx_events { -Q_NAMESPACE - -enum EventType -{ - // Unsupported event - Unsupported, - /// m.room_key_request - KeyRequest, - /// m.room.aliases - Aliases, - /// m.room.avatar - Avatar, - /// m.room.canonical_alias - CanonicalAlias, - /// m.room.create - Create, - /// m.room.encrypted. - Encrypted, - /// m.room.encryption. - Encryption, - /// m.room.guest_access - GuestAccess, - /// m.room.history_visibility - HistoryVisibility, - /// m.room.join_rules - JoinRules, - /// m.room.member - Member, - /// m.room.name - Name, - /// m.room.power_levels - PowerLevels, - /// m.room.tombstone - Tombstone, - /// m.room.topic - Topic, - /// m.room.redaction - Redaction, - /// m.room.pinned_events - PinnedEvents, - // m.sticker - Sticker, - // m.tag - Tag, - /// m.room.message - AudioMessage, - EmoteMessage, - FileMessage, - ImageMessage, - LocationMessage, - NoticeMessage, - TextMessage, - VideoMessage, - Redacted, - UnknownMessage, -}; -Q_ENUM_NS(EventType) - -enum EventState -{ - //! The plaintext message was received by the server. - Received, - //! At least one of the participants has read the message. - Read, - //! The client sent the message. Not yet received. - Sent, - //! When the message is loaded from cache or backfill. - Empty, - //! When the message failed to send - Failed, -}; -Q_ENUM_NS(EventState) -} - -class StateKeeper -{ -public: - StateKeeper(std::function &&fn) - : fn_(std::move(fn)) - {} - - ~StateKeeper() { fn_(); } - -private: - std::function fn_; -}; - -struct DecryptionResult -{ - //! The decrypted content as a normal plaintext event. - mtx::events::collections::TimelineEvents event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; -}; - -class TimelineViewManager; - -class TimelineModel : public QAbstractListModel -{ - Q_OBJECT - Q_PROPERTY( - int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) - -public: - explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0); - - enum Roles - { - Section, - Type, - Body, - FormattedBody, - UserId, - UserName, - Timestamp, - Url, - ThumbnailUrl, - Filename, - Filesize, - MimeType, - Height, - Width, - ProportionalHeight, - Id, - State, - IsEncrypted, - ReplyTo, - }; - - QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - Q_INVOKABLE QColor userColor(QString id, QColor background); - Q_INVOKABLE QString displayName(QString id) const; - Q_INVOKABLE QString avatarUrl(QString id) const; - Q_INVOKABLE QString formatDateSeparator(QDate date) const; - - Q_INVOKABLE QString escapeEmoji(QString str) const; - Q_INVOKABLE void viewRawMessage(QString id) const; - Q_INVOKABLE void openUserProfile(QString userid) const; - Q_INVOKABLE void replyAction(QString id); - Q_INVOKABLE void readReceiptsAction(QString id) const; - Q_INVOKABLE void redactEvent(QString id); - Q_INVOKABLE int idToIndex(QString id) const; - Q_INVOKABLE QString indexToId(int index) const; - - void addEvents(const mtx::responses::Timeline &events); - template - void sendMessage(const T &msg); - -public slots: - void fetchHistory(); - void setCurrentIndex(int index); - int currentIndex() const { return idToIndex(currentId); } - void markEventsAsRead(const std::vector &event_ids); - -private slots: - // Add old events at the top of the timeline. - void addBackwardsEvents(const mtx::responses::Messages &msgs); - -signals: - void oldMessagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(QString txn_id); - void messageSent(QString txn_id, QString event_id); - void currentIndexChanged(int index); - void redactionFailed(QString id); - void eventRedacted(QString id); - -private: - DecryptionResult decryptEvent( - const mtx::events::EncryptedEvent &e) const; - std::vector internalAddEvents( - const std::vector &timeline); - void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content); - void handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_key, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); - void updateLastMessage(); - void readEvent(const std::string &id); - - QHash events; - QSet pending, failed, read; - std::vector eventOrder; - - QString room_id_; - QString prev_batch_token_; - - bool isInitialSync = true; - bool paginationInProgress = false; - - QHash userColors; - QString currentId; - - TimelineViewManager *manager_; -}; - -template -void -TimelineModel::sendMessage(const T &msg) -{ - auto txn_id = http::client()->generate_txn_id(); - mtx::events::RoomEvent msgCopy = {}; - msgCopy.content = msg; - msgCopy.type = mtx::events::EventType::RoomMessage; - msgCopy.event_id = txn_id; - msgCopy.sender = http::client()->user_id().to_string(); - msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - internalAddEvents({msgCopy}); - - QString txn_id_qstr = QString::fromStdString(txn_id); - beginInsertRows(QModelIndex(), - static_cast(this->eventOrder.size()), - static_cast(this->eventOrder.size())); - pending.insert(txn_id_qstr); - this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); - endInsertRows(); - updateLastMessage(); - - if (cache::client()->isRoomEncrypted(room_id_.toStdString())) - sendEncryptedMessage(txn_id, nlohmann::json(msg)); - else - http::client()->send_room_message( - room_id_.toStdString(), - txn_id, - msg, - [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = static_cast(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id_qstr); - } - emit messageSent(txn_id_qstr, - QString::fromStdString(res.event_id.to_string())); - }); -} 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 -- cgit 1.5.1 From 562169965ce68cadbf8214e084477c60ddfdde0b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 9 Nov 2019 03:30:17 +0100 Subject: Show only messages in room list --- src/timeline/TimelineModel.cpp | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) (limited to 'src') 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 -- cgit 1.5.1 From c424e397b01d8191568f951bdb754e1957681fb8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 10 Nov 2019 00:30:02 +0100 Subject: Add loading spinner and restore message send queue --- resources/qml/TimelineView.qml | 13 +++-- src/timeline/TimelineModel.cpp | 97 +++++++++++++++++++++++++++++++++++- src/timeline/TimelineModel.h | 41 ++++----------- src/timeline/TimelineViewManager.cpp | 3 ++ src/timeline/TimelineViewManager.h | 14 +++--- 5 files changed, 123 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index b25b3a7c..3bbaa020 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -19,13 +19,20 @@ Item { color: colors.window Text { - visible: !timelineManager.timeline + visible: !timelineManager.timeline && !timelineManager.isInitialSync anchors.centerIn: parent text: qsTr("No room open") font.pointSize: 24 color: colors.windowText } + BusyIndicator { + anchors.centerIn: parent + running: timelineManager.isInitialSync + height: 200 + width: 200 + } + ListView { id: chat @@ -47,10 +54,6 @@ Item { } else { positionViewAtIndex(model.currentIndex, ListView.End) } - - //if (contentHeight < height) { - // model.fetchHistory(); - //} } } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 9cae4608..6b0057a4 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -332,16 +332,18 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj connect( this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { - pending.remove(txn_id); + pending.removeOne(txn_id); failed.insert(txn_id); int idx = idToIndex(txn_id); if (idx < 0) { nhlog::ui()->warn("Failed index out of range"); return; } + isProcessingPending = false; emit dataChanged(index(idx, 0), index(idx, 0)); }); connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + pending.removeOne(txn_id); int idx = idToIndex(txn_id); if (idx < 0) { nhlog::ui()->warn("Sent index out of range"); @@ -365,11 +367,19 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj // ask to be notified for read receipts cache::client()->addPendingReceipt(room_id_, event_id); + isProcessingPending = false; emit dataChanged(index(idx, 0), index(idx, 0)); + + if (pending.size() > 0) + emit nextPendingMessage(); }); connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); }); + + connect( + this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage); + connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage); } QHash @@ -1035,6 +1045,7 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co } catch (const lmdb::error &e) { nhlog::db()->critical( "failed to save megolm outbound session: {}", e.what()); + emit messageFailed(QString::fromStdString(txn_id)); } }); @@ -1044,13 +1055,14 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co http::client()->query_keys( req, - [keeper = std::move(keeper), megolm_payload, this]( + [keeper = std::move(keeper), megolm_payload, txn_id, this]( const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {} {}", err->matrix_error.error, static_cast(err->status_code)); // TODO: Mark the event as failed. Communicate with the UI. + emit messageFailed(QString::fromStdString(txn_id)); return; } @@ -1150,9 +1162,11 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co } catch (const lmdb::error &e) { nhlog::db()->critical( "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical( "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); } } @@ -1241,3 +1255,82 @@ TimelineModel::handleClaimedKeys(std::shared_ptr keeper, (void)keeper; }); } + +struct SendMessageVisitor +{ + SendMessageVisitor(const QString &txn_id, TimelineModel *model) + : txn_id_qstr_(txn_id) + , model_(model) + {} + + template + void operator()(const mtx::events::Event &) + {} + + template::value, int> = 0> + void operator()(const mtx::events::RoomEvent &msg) + + { + if (cache::client()->isRoomEncrypted(model_->room_id_.toStdString())) { + model_->sendEncryptedMessage(txn_id_qstr_.toStdString(), + nlohmann::json(msg.content)); + } else { + QString txn_id_qstr = txn_id_qstr_; + TimelineModel *model = model_; + http::client()->send_room_message( + model->room_id_.toStdString(), + txn_id_qstr.toStdString(), + msg.content, + [txn_id_qstr, model](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id_qstr.toStdString(), + err->matrix_error.error, + status_code); + emit model->messageFailed(txn_id_qstr); + } + emit model->messageSent( + txn_id_qstr, QString::fromStdString(res.event_id.to_string())); + }); + } + } + + QString txn_id_qstr_; + TimelineModel *model_; +}; + +void +TimelineModel::processOnePendingMessage() +{ + if (isProcessingPending || pending.isEmpty()) + return; + + isProcessingPending = true; + + QString txn_id_qstr = pending.first(); + + boost::apply_visitor(SendMessageVisitor{txn_id_qstr, this}, events.value(txn_id_qstr)); +} + +void +TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) +{ + internalAddEvents({event}); + + QString txn_id_qstr = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, event); + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size())); + pending.push_back(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); + endInsertRows(); + updateLastMessage(); + + if (!isProcessingPending) + emit nextPendingMessage(); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 31e41315..e7842b99 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -173,6 +173,8 @@ public slots: private slots: // Add old events at the top of the timeline. void addBackwardsEvents(const mtx::responses::Messages &msgs); + void processOnePendingMessage(); + void addPendingMessage(mtx::events::collections::TimelineEvents event); signals: void oldMessagesRetrieved(const mtx::responses::Messages &res); @@ -181,6 +183,8 @@ signals: void currentIndexChanged(int index); void redactionFailed(QString id); void eventRedacted(QString id); + void nextPendingMessage(); + void newMessageToSend(mtx::events::collections::TimelineEvents event); private: DecryptionResult decryptEvent( @@ -198,7 +202,8 @@ private: void readEvent(const std::string &id); QHash events; - QSet pending, failed, read; + QSet failed, read; + QList pending; std::vector eventOrder; QString room_id_; @@ -206,11 +211,14 @@ private: bool isInitialSync = true; bool paginationInProgress = false; + bool isProcessingPending = false; QHash userColors; QString currentId; TimelineViewManager *manager_; + + friend struct SendMessageVisitor; }; template @@ -224,35 +232,6 @@ TimelineModel::sendMessage(const T &msg) msgCopy.event_id = txn_id; msgCopy.sender = http::client()->user_id().to_string(); msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - internalAddEvents({msgCopy}); - - QString txn_id_qstr = QString::fromStdString(txn_id); - beginInsertRows(QModelIndex(), - static_cast(this->eventOrder.size()), - static_cast(this->eventOrder.size())); - pending.insert(txn_id_qstr); - this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); - endInsertRows(); - updateLastMessage(); - if (cache::client()->isRoomEncrypted(room_id_.toStdString())) - sendEncryptedMessage(txn_id, nlohmann::json(msg)); - else - http::client()->send_room_message( - room_id_.toStdString(), - txn_id, - msg, - [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = static_cast(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id_qstr); - } - emit messageSent(txn_id_qstr, - QString::fromStdString(res.event_id.to_string())); - }); + emit newMessageToSend(msgCopy); } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index d733ad90..06c42a39 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -97,6 +97,9 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms) addRoom(QString::fromStdString(it->first)); models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline); } + + this->isInitialSync_ = false; + emit initialSyncChanged(false); } void diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 691c8ddb..0bc58e68 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -12,10 +12,6 @@ #include "TimelineModel.h" #include "Utils.h" -// temporary for stubs -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - class MxcImageProvider; class ColorImageProvider; @@ -25,6 +21,8 @@ class TimelineViewManager : public QObject Q_PROPERTY( TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) + Q_PROPERTY( + bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) public: TimelineViewManager(QWidget *parent = 0); @@ -36,6 +34,7 @@ public: void clearAll() { models.clear(); } Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } + Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } void openImageOverlay(QString mxcUrl, QString originalFilename, QString mimeType, @@ -66,6 +65,7 @@ signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); + void initialSyncChanged(bool isInitialSync); void mediaCached(QString mxcUrl, QString cacheUrl); public slots: @@ -107,11 +107,11 @@ private: QQuickWidget *view; #endif QWidget *container; - TimelineModel *timeline_ = nullptr; + MxcImageProvider *imgProvider; ColorImageProvider *colorImgProvider; QHash> models; + TimelineModel *timeline_ = nullptr; + bool isInitialSync_ = true; }; - -#pragma GCC diagnostic pop -- cgit 1.5.1 From 001c94865c98836b06c827ff890a5589dd97320d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 12 Nov 2019 15:10:59 +0000 Subject: 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(-) (limited to 'src') 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()); -- cgit 1.5.1 From cf88499ccb6709db3312cd675c87614389cc0aac Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 11 Nov 2019 19:58:20 +0100 Subject: Fix replies to encrypted events --- src/timeline/TimelineModel.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') 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_ = {}; -- cgit 1.5.1 From 5429b425e97f3482e7e5510a8606659168f58b7d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 12 Nov 2019 17:46:09 +0100 Subject: Lint --- src/timeline/TimelineModel.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'src') 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; } -- cgit 1.5.1 From 7bd875004f6995865a71a55facf56834c91bfb48 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 22 Nov 2019 16:36:45 +0100 Subject: Only mark messages as read, when room is active --- src/timeline/TimelineModel.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') 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()); } } -- cgit 1.5.1 From 9fd279c020bba2f433a0f9862277bc59fd621130 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 22 Nov 2019 17:08:32 +0100 Subject: 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/Pill.qml | 14 ++++++++++++++ resources/qml/delegates/PlayableMediaMessage.qml | 2 +- resources/qml/delegates/Redacted.qml | 15 --------------- resources/res.qrc | 2 +- src/timeline/TimelineModel.cpp | 5 ++++- 7 files changed, 30 insertions(+), 20 deletions(-) create mode 100644 resources/qml/delegates/Pill.qml delete mode 100644 resources/qml/delegates/Redacted.qml (limited to 'src') 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/Pill.qml b/resources/qml/delegates/Pill.qml new file mode 100644 index 00000000..53a9684e --- /dev/null +++ b/resources/qml/delegates/Pill.qml @@ -0,0 +1,14 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 + +Label { + 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/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/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml deleted file mode 100644 index 42fb4835..00000000 --- a/resources/qml/delegates/Redacted.qml +++ /dev/null @@ -1,15 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 2.1 - -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 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.; } } -- cgit 1.5.1 From 6c2ec3fe67d6230cf992b0eca9362789987111fb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 30 Nov 2019 01:43:39 +0100 Subject: Rename qml namespace from com.github.nheko to im.nheko --- resources/qml/EncryptionIndicator.qml | 2 +- resources/qml/StatusIndicator.qml | 2 +- resources/qml/TimelineRow.qml | 2 +- resources/qml/TimelineView.qml | 2 +- resources/qml/delegates/ImageMessage.qml | 2 +- resources/qml/delegates/MessageDelegate.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 2 +- src/timeline/TimelineViewManager.cpp | 6 +++--- 8 files changed, 10 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index 2cd9161b..905cf934 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 -import com.github.nheko 1.0 +import im.nheko 1.0 Rectangle { id: indicator diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 2ed59a17..91e8f769 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 -import com.github.nheko 1.0 +import im.nheko 1.0 Rectangle { id: indicator diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index a2d4fca5..4917e893 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -3,7 +3,7 @@ import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 -import com.github.nheko 1.0 +import im.nheko 1.0 import "./delegates" diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index a5520031..b97af0dd 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -4,7 +4,7 @@ import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 -import com.github.nheko 1.0 +import im.nheko 1.0 import "./delegates" diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 802ef721..a1a06012 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -1,6 +1,6 @@ import QtQuick 2.6 -import com.github.nheko 1.0 +import im.nheko 1.0 Item { width: Math.min(parent ? parent.width : undefined, model.width) diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index e31321f9..178dfd86 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -1,5 +1,5 @@ import QtQuick 2.6 -import com.github.nheko 1.0 +import im.nheko 1.0 DelegateChooser { //role: "type" //< not supported in our custom implementation, have to use roleValue diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 1207ac77..3b987545 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts 1.2 import QtQuick.Controls 2.1 import QtMultimedia 5.6 -import com.github.nheko 1.0 +import im.nheko 1.0 Rectangle { id: bg diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 39bdfcf4..2a88c882 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -55,13 +55,13 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) , colorImgProvider(new ColorImageProvider()) { qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, - "com.github.nheko", + "im.nheko", 1, 0, "MtxEvent", "Can't instantiate enum!"); - qmlRegisterType("com.github.nheko", 1, 0, "DelegateChoice"); - qmlRegisterType("com.github.nheko", 1, 0, "DelegateChooser"); + qmlRegisterType("im.nheko", 1, 0, "DelegateChoice"); + qmlRegisterType("im.nheko", 1, 0, "DelegateChooser"); #ifdef USE_QUICK_VIEW view = new QQuickView(); -- cgit 1.5.1 From b8f6e4ce6462f074c34a8b7a286cbabe0e2897aa Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Dec 2019 02:26:41 +0100 Subject: Add encrypted file download --- deps/CMakeLists.txt | 4 +- resources/qml/TimelineRow.qml | 2 +- resources/qml/delegates/FileMessage.qml | 2 +- resources/qml/delegates/ImageMessage.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 4 +- src/timeline/TimelineModel.cpp | 184 +++++++++++++++++++++++ src/timeline/TimelineModel.h | 3 + src/timeline/TimelineViewManager.cpp | 154 ++----------------- src/timeline/TimelineViewManager.h | 27 +--- 9 files changed, 210 insertions(+), 172 deletions(-) (limited to 'src') diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index d0a715e0..c5932ab7 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -46,10 +46,10 @@ set(BOOST_SHA256 set( MTXCLIENT_URL - https://github.com/Nheko-Reborn/mtxclient/archive/6eee767cc25a9db9f125843e584656cde1ebb6c5.tar.gz + https://github.com/Nheko-Reborn/mtxclient/archive/f719236b08d373d9508f2467bbfc6dfa953b1f8d.zip ) set(MTXCLIENT_HASH - 72fe77da4fed98b3cf069299f66092c820c900359a27ec26070175f9ad208a03) + 0660756c16cf297e02b0b29c07a59fc851723cc65f305893ae7238e6dd2e41c8) set( TWEENY_URL https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 4917e893..2c2ed02a 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -97,7 +97,7 @@ RowLayout { MenuItem { visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage || model.type == MtxEvent.Sticker text: qsTr("Save as") - onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) + onTriggered: timelineManager.timeline.saveMedia(model.id) } } } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index f4cf3f15..2c911c5e 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -31,7 +31,7 @@ Rectangle { } MouseArea { anchors.fill: parent - onClicked: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) + onClicked: timelineManager.timeline.saveMedia(model.id) cursorShape: Qt.PointingHandCursor } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index a1a06012..1b6e5729 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -17,7 +17,7 @@ Item { MouseArea { enabled: model.type == MtxEvent.ImageMessage anchors.fill: parent - onClicked: timelineManager.openImageOverlay(model.url, model.filename, model.mimetype, model.type) + onClicked: timelineManager.openImageOverlay(model.url, model.id) } } } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 3b987545..d0d4d7cb 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -97,7 +97,7 @@ Rectangle { anchors.fill: parent onClicked: { switch (button.state) { - case "": timelineManager.cacheMedia(model.url, model.mimetype); break; + case "": timelineManager.timeline.cacheMedia(model.id); break; case "stopped": media.play(); console.log("play"); button.state = "playing" @@ -118,7 +118,7 @@ Rectangle { } Connections { - target: timelineManager + target: timelineManager.timeline onMediaCached: { if (mxcUrl == model.url) { media.source = "file://" + cacheUrl diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index b904dfd7..f606b603 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -3,11 +3,15 @@ #include #include +#include +#include #include +#include #include "ChatPage.h" #include "Logging.h" #include "MainWindow.h" +#include "MxcImageProvider.h" #include "Olm.h" #include "TimelineViewManager.h" #include "Utils.h" @@ -88,17 +92,42 @@ eventFormattedBody(const mtx::events::RoomEvent &e) } } +template +boost::optional +eventEncryptionInfo(const mtx::events::Event &) +{ + return boost::none; +} + +template +auto +eventEncryptionInfo(const mtx::events::RoomEvent &e) -> std::enable_if_t< + std::is_same>::value, + boost::optional> +{ + return e.content.file; +} + template QString eventUrl(const mtx::events::Event &) { return ""; } + +QString +eventUrl(const mtx::events::StateEvent &e) +{ + return QString::fromStdString(e.content.url); +} + template auto eventUrl(const mtx::events::RoomEvent &e) -> std::enable_if_t::value, QString> { + if (e.content.file) + return QString::fromStdString(e.content.file->url); return QString::fromStdString(e.content.url); } @@ -1342,3 +1371,158 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) if (!isProcessingPending) emit nextPendingMessage(); } + +void +TimelineModel::saveMedia(QString eventId) const +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = + boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event); + QString originalFilename = + boost::apply_visitor([](const auto &e) -> QString { return eventFilename(e); }, event); + QString mimeType = + boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event); + + using EncF = boost::optional; + EncF encryptionInfo = + boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event); + + qml_mtx_events::EventType eventType = boost::apply_visitor( + [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, event); + + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); + } + + QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + + auto filename = QFileDialog::getSaveFileName( + manager_->getWidget(), dialogTitle, originalFilename, filterString); + + if (filename.isEmpty()) + return; + + const auto url = mxcUrl.toStdString(); + + http::client()->download( + url, + [filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), (int)temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); +} + +void +TimelineModel::cacheMedia(QString eventId) +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = + boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event); + QString mimeType = + boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event); + + using EncF = boost::optional; + EncF encryptionInfo = + boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event); + + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString(mxcUrl).remove("mxc://")) + .arg(suffix)); + if (QDir::cleanPath(filename.path()) != filename.path()) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } + + QDir().mkpath(filename.path()); + + if (filename.isReadable()) { + emit mediaCached(mxcUrl, filename.filePath()); + return; + } + + http::client()->download( + url, + [this, mxcUrl, filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + + emit mediaCached(mxcUrl, filename.filePath()); + }); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index e7842b99..f52091e6 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -159,6 +159,8 @@ public: Q_INVOKABLE void redactEvent(QString id); Q_INVOKABLE int idToIndex(QString id) const; Q_INVOKABLE QString indexToId(int index) const; + Q_INVOKABLE void cacheMedia(QString eventId); + Q_INVOKABLE void saveMedia(QString eventId) const; void addEvents(const mtx::responses::Timeline &events); template @@ -185,6 +187,7 @@ signals: void eventRedacted(QString id); void nextPendingMessage(); void newMessageToSend(mtx::events::collections::TimelineEvents event); + void mediaCached(QString mxcUrl, QString cacheUrl); private: DecryptionResult decryptEvent( diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 2a88c882..6430a426 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -1,11 +1,8 @@ #include "TimelineViewManager.h" -#include #include -#include #include #include -#include #include "ChatPage.h" #include "ColorImageProvider.h" @@ -124,146 +121,24 @@ TimelineViewManager::setHistoryView(const QString &room_id) } void -TimelineViewManager::openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const +TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { QQuickImageResponse *imgResponse = imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); - connect(imgResponse, - &QQuickImageResponse::finished, - this, - [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() { - if (!imgResponse->errorString().isEmpty()) { - nhlog::ui()->error("Error when retrieving image for overlay: {}", - imgResponse->errorString().toStdString()); - return; - } - auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - - auto imgDialog = new dialogs::ImageOverlay(pixmap); - imgDialog->show(); - connect(imgDialog, - &dialogs::ImageOverlay::saving, - this, - [this, mxcUrl, originalFilename, mimeType, eventType]() { - saveMedia(mxcUrl, originalFilename, mimeType, eventType); - }); - }); -} - -void -TimelineViewManager::saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const -{ - QString dialogTitle; - if (eventType == qml_mtx_events::EventType::ImageMessage) { - dialogTitle = tr("Save image"); - } else if (eventType == qml_mtx_events::EventType::VideoMessage) { - dialogTitle = tr("Save video"); - } else if (eventType == qml_mtx_events::EventType::AudioMessage) { - dialogTitle = tr("Save audio"); - } else { - dialogTitle = tr("Save file"); - } - - QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); - - auto filename = - QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString); - - if (filename.isEmpty()) - return; - - const auto url = mxcUrl.toStdString(); - - http::client()->download( - url, - [filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), (int)data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - }); -} - -void -TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) -{ - // If the message is a link to a non mxcUrl, don't download it - if (!mxcUrl.startsWith("mxc://")) { - emit mediaCached(mxcUrl, mxcUrl); - return; - } - - QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); - - const auto url = mxcUrl.toStdString(); - QFileInfo filename(QString("%1/media_cache/%2.%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString(mxcUrl).remove("mxc://")) - .arg(suffix)); - if (QDir::cleanPath(filename.path()) != filename.path()) { - nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); - return; - } - - QDir().mkpath(filename.path()); - - if (filename.isReadable()) { - emit mediaCached(mxcUrl, filename.filePath()); - return; - } + connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, imgResponse]() { + if (!imgResponse->errorString().isEmpty()) { + nhlog::ui()->error("Error when retrieving image for overlay: {}", + imgResponse->errorString().toStdString()); + return; + } + auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - http::client()->download( - url, - [this, mxcUrl, filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename.filePath()); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - - emit mediaCached(mxcUrl, filename.filePath()); - }); + auto imgDialog = new dialogs::ImageOverlay(pixmap); + imgDialog->show(); + connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() { + timeline_->saveMedia(eventId); + }); + }); } void @@ -401,3 +276,4 @@ TimelineViewManager::queueVideoMessage(const QString &roomid, video.url = url.toStdString(); models.value(roomid)->sendMessage(video); } + diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 0bc58e68..1cb0de44 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -35,38 +35,13 @@ public: Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } - void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType); - // Qml can only pass enum as int - Q_INVOKABLE void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - openImageOverlay( - mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } - Q_INVOKABLE void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } + Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); void initialSyncChanged(bool isInitialSync); - void mediaCached(QString mxcUrl, QString cacheUrl); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); -- cgit 1.5.1 From a689118d71000adba36de19e8ad022ec69695627 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Dec 2019 19:49:56 +0100 Subject: lint --- src/timeline/TimelineViewManager.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 6430a426..c44bcbbf 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -276,4 +276,3 @@ TimelineViewManager::queueVideoMessage(const QString &roomid, video.url = url.toStdString(); models.value(roomid)->sendMessage(video); } - -- cgit 1.5.1 From 5bfdaff7780bc4299c3edab85c688eebf21f7d4e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Dec 2019 23:34:16 +0100 Subject: Implement decryption of images It is a bit of a hack, but it works... --- CMakeLists.txt | 1 + src/MxcImageProvider.cpp | 9 +++++++-- src/MxcImageProvider.h | 30 ++++++++++++++++++++++++++---- src/timeline/TimelineModel.cpp | 13 +++++++++++++ src/timeline/TimelineModel.h | 2 ++ src/timeline/TimelineViewManager.cpp | 11 ++++++++--- 6 files changed, 57 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/CMakeLists.txt b/CMakeLists.txt index c918d834..67a1dfb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -365,6 +365,7 @@ qt5_wrap_cpp(MOC_HEADERS src/CommunitiesList.h src/LoginPage.h src/MainWindow.h + src/MxcImageProvider.h src/InviteeItem.h src/QuickSwitcher.h src/RegisterPage.h diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index 556b019b..edf6ceb5 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -5,7 +5,7 @@ void MxcImageResponse::run() { - if (m_requestedSize.isValid()) { + if (m_requestedSize.isValid() && !m_encryptionInfo) { QString fileName = QString("%1_%2x%3_crop") .arg(m_id) .arg(m_requestedSize.width()) @@ -65,7 +65,12 @@ MxcImageResponse::run() return; } - auto data = QByteArray(res.data(), res.size()); + auto temp = res; + if (m_encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, m_encryptionInfo.value())); + + auto data = QByteArray(temp.data(), temp.size()); m_image.loadFromData(data); m_image.setText("original filename", QString::fromStdString(originalFilename)); diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index 19d8a74e..2c197a13 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -6,14 +6,21 @@ #include #include +#include + +#include + class MxcImageResponse : public QQuickImageResponse , public QRunnable { public: - MxcImageResponse(const QString &id, const QSize &requestedSize) + MxcImageResponse(const QString &id, + const QSize &requestedSize, + boost::optional encryptionInfo) : m_id(id) , m_requestedSize(requestedSize) + , m_encryptionInfo(encryptionInfo) { setAutoDelete(false); } @@ -29,19 +36,34 @@ public: QString m_id, m_error; QSize m_requestedSize; QImage m_image; + boost::optional m_encryptionInfo; }; -class MxcImageProvider : public QQuickAsyncImageProvider +class MxcImageProvider + : public QObject + , public QQuickAsyncImageProvider { -public: + Q_OBJECT +public slots: QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override { - MxcImageResponse *response = new MxcImageResponse(id, requestedSize); + boost::optional info; + auto temp = infos.find("mxc://" + id); + if (temp != infos.end()) + info = *temp; + + MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info); pool.start(response); return response; } + void addEncryptionInfo(mtx::crypto::EncryptedFile info) + { + infos.insert(QString::fromStdString(info.url), info); + } + private: QThreadPool pool; + QHash infos; }; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index f606b603..2c58e2f5 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -673,6 +673,19 @@ TimelineModel::internalAddEvents( continue; // don't insert redaction into timeline } + if (auto event = + boost::get>(&e)) { + auto temp = decryptEvent(*event).event; + auto encInfo = boost::apply_visitor( + [](const auto &ev) -> boost::optional { + return eventEncryptionInfo(ev); + }, + temp); + + if (encInfo) + emit newEncryptedImage(encInfo.value()); + } + this->events.insert(id, e); ids.push_back(id); } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index f52091e6..06c64acf 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -6,6 +6,7 @@ #include #include +#include #include #include "Cache.h" @@ -188,6 +189,7 @@ signals: void nextPendingMessage(); void newMessageToSend(mtx::events::collections::TimelineEvents event); void mediaCached(QString mxcUrl, QString cacheUrl); + void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); private: DecryptionResult decryptEvent( diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index c44bcbbf..25f72a6d 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -102,9 +102,14 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms) void TimelineViewManager::addRoom(const QString &room_id) { - if (!models.contains(room_id)) - models.insert(room_id, - QSharedPointer(new TimelineModel(this, room_id))); + if (!models.contains(room_id)) { + QSharedPointer newRoom(new TimelineModel(this, room_id)); + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + imgProvider, + &MxcImageProvider::addEncryptionInfo); + models.insert(room_id, std::move(newRoom)); + } } void -- cgit 1.5.1 From 43d7fe0d358edd1983257350817f7e76132c8dc8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 5 Dec 2019 15:31:53 +0100 Subject: Implement sending encrypted files --- src/ChatPage.cpp | 206 ++++++++--------------------------- src/ChatPage.h | 21 +--- src/TextInputWidget.cpp | 28 ++--- src/TextInputWidget.h | 12 +- src/timeline/TimelineViewManager.cpp | 19 +++- src/timeline/TimelineViewManager.h | 5 + 6 files changed, 79 insertions(+), 212 deletions(-) (limited to 'src') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 091a9fa0..d6f6940b 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -54,6 +54,8 @@ constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; constexpr int RETRY_TIMEOUT = 5'000; constexpr size_t MAX_ONETIME_KEYS = 50; +Q_DECLARE_METATYPE(boost::optional) + ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) : QWidget(parent) , isConnected_(true) @@ -62,6 +64,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) { setObjectName("chatPage"); + qRegisterMetaType>( + "boost::optional"); + topLayout_ = new QHBoxLayout(this); topLayout_->setSpacing(0); topLayout_->setMargin(0); @@ -299,9 +304,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect( text_input_, - &TextInputWidget::uploadImage, + &TextInputWidget::uploadMedia, this, - [this](QSharedPointer dev, const QString &fn) { + [this](QSharedPointer dev, QString mimeClass, const QString &fn) { QMimeDatabase db; QMimeType mime = db.mimeTypeForData(dev.data()); @@ -311,9 +316,18 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) return; } - auto bin = dev->peek(dev->size()); - auto payload = std::string(bin.data(), bin.size()); - auto dimensions = QImageReader(dev.data()).size(); + auto bin = dev->peek(dev->size()); + auto payload = std::string(bin.data(), bin.size()); + boost::optional encryptedFile; + if (cache::client()->isRoomEncrypted(current_room_.toStdString())) { + mtx::crypto::BinaryBuf buf; + std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload); + payload = mtx::crypto::to_string(buf); + } + + QSize dimensions; + if (mimeClass == "image") + dimensions = QImageReader(dev.data()).size(); http::client()->upload( payload, @@ -322,193 +336,61 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) [this, room_id = current_room_, filename = fn, - mime = mime.name(), - size = payload.size(), + encryptedFile, + mimeClass, + mime = mime.name(), + size = payload.size(), dimensions](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { if (err) { emit uploadFailed( - tr("Failed to upload image. Please try again.")); - nhlog::net()->warn("failed to upload image: {} {} ({})", + tr("Failed to upload media. Please try again.")); + nhlog::net()->warn("failed to upload media: {} {} ({})", err->matrix_error.error, to_string(err->matrix_error.errcode), static_cast(err->status_code)); return; } - emit imageUploaded(room_id, + emit mediaUploaded(room_id, filename, + encryptedFile, QString::fromStdString(res.content_uri), + mimeClass, mime, size, dimensions); }); }); - connect(text_input_, - &TextInputWidget::uploadFile, - this, - [this](QSharedPointer dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload file. Please try again.")); - nhlog::net()->warn("failed to upload file: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - emit fileUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - - connect(text_input_, - &TextInputWidget::uploadAudio, - this, - [this](QSharedPointer dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload audio. Please try again.")); - nhlog::net()->warn("failed to upload audio: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - emit audioUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - connect(text_input_, - &TextInputWidget::uploadVideo, - this, - [this](QSharedPointer dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload video. Please try again.")); - nhlog::net()->warn("failed to upload video: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - emit videoUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) { text_input_->hideUploadSpinner(); emit showNotification(msg); }); connect(this, - &ChatPage::imageUploaded, + &ChatPage::mediaUploaded, this, [this](QString roomid, QString filename, + boost::optional encryptedFile, QString url, + QString mimeClass, QString mime, qint64 dsize, QSize dimensions) { text_input_->hideUploadSpinner(); - view_manager_->queueImageMessage( - roomid, filename, url, mime, dsize, dimensions); - }); - connect(this, - &ChatPage::fileUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); - }); - connect(this, - &ChatPage::audioUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize); - }); - connect(this, - &ChatPage::videoUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize); + + if (mimeClass == "image") + view_manager_->queueImageMessage( + roomid, filename, encryptedFile, url, mime, dsize, dimensions); + else if (mimeClass == "audio") + view_manager_->queueAudioMessage( + roomid, filename, encryptedFile, url, mime, dsize); + else if (mimeClass == "video") + view_manager_->queueVideoMessage( + roomid, filename, encryptedFile, url, mime, dsize); + else + view_manager_->queueFileMessage( + roomid, filename, encryptedFile, url, mime, dsize); }); connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar); diff --git a/src/ChatPage.h b/src/ChatPage.h index 1898f1a7..20e156af 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -18,7 +18,9 @@ #pragma once #include +#include #include +#include #include #include @@ -94,27 +96,14 @@ signals: const QPoint widgetPos); void uploadFailed(const QString &msg); - void imageUploaded(const QString &roomid, + void mediaUploaded(const QString &roomid, const QString &filename, + const boost::optional &file, const QString &url, + const QString &mimeClass, const QString &mime, qint64 dsize, const QSize &dimensions); - void fileUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); - void audioUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); - void videoUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); void contentLoaded(); void closing(); diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index f723c01a..66700dbc 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -458,21 +458,16 @@ FilteredTextEdit::textChanged() } void -FilteredTextEdit::uploadData(const QByteArray data, const QString &media, const QString &filename) +FilteredTextEdit::uploadData(const QByteArray data, + const QString &mediaType, + const QString &filename) { QSharedPointer buffer{new QBuffer{this}}; buffer->setData(data); emit startedUpload(); - if (media == "image") - emit image(buffer, filename); - else if (media == "audio") - emit audio(buffer, filename); - else if (media == "video") - emit video(buffer, filename); - else - emit file(buffer, filename); + emit media(buffer, mediaType, filename); } void @@ -580,10 +575,7 @@ TextInputWidget::TextInputWidget(QWidget *parent) connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage); connect(input_, &FilteredTextEdit::reply, this, &TextInputWidget::sendReplyMessage); connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command); - connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage); - connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio); - connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo); - connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile); + connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia); connect(emojiBtn_, SIGNAL(emojiSelected(const QString &)), this, @@ -642,14 +634,8 @@ TextInputWidget::openFileSelection() const auto format = mime.name().split("/")[0]; QSharedPointer file{new QFile{fileName, this}}; - if (format == "image") - emit uploadImage(file, fileName); - else if (format == "audio") - emit uploadAudio(file, fileName); - else if (format == "video") - emit uploadVideo(file, fileName); - else - emit uploadFile(file, fileName); + + emit uploadMedia(file, format, fileName); showUploadSpinner(); } diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 71f794d1..d498be72 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -63,10 +63,7 @@ signals: void message(QString); void reply(QString, const RelatedInfo &); void command(QString name, QString args); - void image(QSharedPointer data, const QString &filename); - void audio(QSharedPointer data, const QString &filename); - void video(QSharedPointer data, const QString &filename); - void file(QSharedPointer data, const QString &filename); + void media(QSharedPointer data, QString mimeClass, const QString &filename); //! Trigger the suggestion popup. void showSuggestions(const QString &query); @@ -179,10 +176,9 @@ signals: void sendEmoteMessage(QString msg); void heightChanged(int height); - void uploadImage(const QSharedPointer data, const QString &filename); - void uploadFile(const QSharedPointer data, const QString &filename); - void uploadAudio(const QSharedPointer data, const QString &filename); - void uploadVideo(const QSharedPointer data, const QString &filename); + void uploadMedia(const QSharedPointer data, + QString mimeClass, + const QString &filename); void sendJoinRoomRequest(const QString &room); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 25f72a6d..6e18d111 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -222,6 +222,7 @@ TimelineViewManager::queueEmoteMessage(const QString &msg) void TimelineViewManager::queueImageMessage(const QString &roomid, const QString &filename, + const boost::optional &file, const QString &url, const QString &mime, uint64_t dsize, @@ -234,27 +235,32 @@ TimelineViewManager::queueImageMessage(const QString &roomid, image.url = url.toStdString(); image.info.h = dimensions.height(); image.info.w = dimensions.width(); + image.file = file; models.value(roomid)->sendMessage(image); } void -TimelineViewManager::queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize) +TimelineViewManager::queueFileMessage( + const QString &roomid, + const QString &filename, + const boost::optional &encryptedFile, + 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(); + file.file = encryptedFile; models.value(roomid)->sendMessage(file); } void TimelineViewManager::queueAudioMessage(const QString &roomid, const QString &filename, + const boost::optional &file, const QString &url, const QString &mime, uint64_t dsize) @@ -264,12 +270,14 @@ TimelineViewManager::queueAudioMessage(const QString &roomid, audio.info.size = dsize; audio.body = filename.toStdString(); audio.url = url.toStdString(); + audio.file = file; models.value(roomid)->sendMessage(audio); } void TimelineViewManager::queueVideoMessage(const QString &roomid, const QString &filename, + const boost::optional &file, const QString &url, const QString &mime, uint64_t dsize) @@ -279,5 +287,6 @@ TimelineViewManager::queueVideoMessage(const QString &roomid, video.info.size = dsize; video.body = filename.toStdString(); video.url = url.toStdString(); + video.file = file; models.value(roomid)->sendMessage(video); } diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 1cb0de44..9e8de616 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -5,6 +5,7 @@ #include #include +#include #include #include "Cache.h" @@ -55,22 +56,26 @@ public slots: void queueEmoteMessage(const QString &msg); void queueImageMessage(const QString &roomid, const QString &filename, + const boost::optional &file, const QString &url, const QString &mime, uint64_t dsize, const QSize &dimensions); void queueFileMessage(const QString &roomid, const QString &filename, + const boost::optional &file, const QString &url, const QString &mime, uint64_t dsize); void queueAudioMessage(const QString &roomid, const QString &filename, + const boost::optional &file, const QString &url, const QString &mime, uint64_t dsize); void queueVideoMessage(const QString &roomid, const QString &filename, + const boost::optional &file, const QString &url, const QString &mime, uint64_t dsize); -- cgit 1.5.1 From 362efbf5b9ca4d3162d4fc7035959ce1da9f1c94 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 6 Dec 2019 02:56:53 +0100 Subject: Restore linkification of messages --- src/timeline/TimelineModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 2c58e2f5..e3d87ae6 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -505,8 +505,8 @@ TimelineModel::data(const QModelIndex &index, int role) const case FormattedBody: return QVariant( utils::replaceEmoji( - boost::apply_visitor( - [](const auto &e) -> QString { return eventFormattedBody(e); }, event)) + utils::linkifyMessage(boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event))) .remove("") .remove("")); case Url: -- cgit 1.5.1