From ed9501023ae57e668a930e5d3accbb47ad3d7812 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Sat, 5 May 2018 16:38:41 +0300 Subject: [PATCH] Add support for retrieving the notification events (#33) --- cmake/MatrixStructs.cmake | 2 +- include/Cache.h | 7 +++++ include/ChatPage.h | 2 ++ include/MatrixClient.h | 2 ++ include/Utils.h | 34 +++++++++++++++++++++++++ include/timeline/TimelineView.h | 3 --- src/Cache.cc | 45 ++++++++++++++++++++++++++++----- src/ChatPage.cc | 38 ++++++++++++++++++++++++++++ src/MatrixClient.cc | 38 ++++++++++++++++++++++++++++ src/Utils.cc | 27 ++++++++++++++++++++ src/timeline/TimelineView.cc | 20 +++------------ 11 files changed, 192 insertions(+), 26 deletions(-) diff --git a/cmake/MatrixStructs.cmake b/cmake/MatrixStructs.cmake index 02c0c9d0..cf8e4710 100644 --- a/cmake/MatrixStructs.cmake +++ b/cmake/MatrixStructs.cmake @@ -21,7 +21,7 @@ ExternalProject_Add( MatrixStructs GIT_REPOSITORY https://github.com/mujx/matrix-structs - GIT_TAG 690080daa3bc1984297c4d7103cde9ea07e2e0b7 + GIT_TAG 55a1a5aad0ead3cc45475fc1aed1bf54a56e352c BUILD_IN_SOURCE 1 SOURCE_DIR ${MATRIX_STRUCTS_ROOT} diff --git a/include/Cache.h b/include/Cache.h index 0829acf5..1fa6c430 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -233,6 +233,12 @@ public: std::vector searchRooms(const std::string &query, std::uint8_t max_items = 5); + void markSentNotification(const std::string &event_id); + //! Removes an event from the sent notifications. + void removeReadNotification(const std::string &event_id); + //! Check if we have sent a desktop notification for the given event id. + bool isNotificationSent(const std::string &event_id); + private: //! Save an invited room. void saveInvite(lmdb::txn &txn, @@ -422,6 +428,7 @@ private: lmdb::dbi invitesDb_; lmdb::dbi mediaDb_; lmdb::dbi readReceiptsDb_; + lmdb::dbi notificationsDb_; QString localUserId_; QString cacheDirectory_; diff --git a/include/ChatPage.h b/include/ChatPage.h index 147ff6b2..f659163c 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h @@ -136,6 +136,8 @@ private: //! Update the room with the new notification count. void updateRoomNotificationCount(const QString &room_id, uint16_t notification_count); + //! Send desktop notification for the received messages. + void sendDesktopNotifications(const mtx::responses::Notifications &); QStringList generateTypingUsers(const QString &room_id, const std::vector &typing_users); diff --git a/include/MatrixClient.h b/include/MatrixClient.h index 1be15e56..35f05c31 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -91,6 +91,7 @@ public: void redactEvent(const QString &room_id, const QString &event_id); void inviteUser(const QString &room_id, const QString &user); void createRoom(const mtx::requests::CreateRoom &request); + void getNotifications() noexcept; QUrl getHomeServer() { return server_; }; int transactionId() { return txn_id_; }; @@ -178,6 +179,7 @@ signals: void redactionCompleted(const QString &room_id, const QString &event_id); void invalidToken(); void syncError(const QString &error); + void notificationsRetrieved(const mtx::responses::Notifications ¬ifications); private: QNetworkReply *makeUploadRequest(QSharedPointer iodev); diff --git a/include/Utils.h b/include/Utils.h index c9dc460a..6fea4962 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -32,6 +32,9 @@ firstChar(const QString &input); QString humanReadableFileSize(uint64_t bytes); +QString +event_body(const mtx::events::collections::TimelineEvents &event); + //! Match widgets/events with a description message. template QString @@ -131,6 +134,37 @@ erase_if(ContainerT &items, const PredicateT &predicate) } } +inline mtx::events::EventType +event_type(const mtx::events::collections::TimelineEvents &event) +{ + return mpark::visit([](auto msg) { return msg.type; }, event); +} + +inline std::string +event_id(const mtx::events::collections::TimelineEvents &event) +{ + return mpark::visit([](auto msg) { return msg.event_id; }, event); +} + +inline QString +eventId(const mtx::events::collections::TimelineEvents &event) +{ + return QString::fromStdString(event_id(event)); +} + +inline QString +event_sender(const mtx::events::collections::TimelineEvents &event) +{ + return mpark::visit([](auto msg) { return QString::fromStdString(msg.sender); }, event); +} + +template +QString +message_body(const mtx::events::collections::TimelineEvents &event) +{ + return QString::fromStdString(mpark::get(event).content.body); +} + //! Calculate the Levenshtein distance between two strings with character skipping. int levenshtein_distance(const std::string &s1, const std::string &s2); diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h index ab4fbd47..02e2872a 100644 --- a/include/timeline/TimelineView.h +++ b/include/timeline/TimelineView.h @@ -211,9 +211,6 @@ private: bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; } //! Retrieve the event id of the last item. QString getLastEventId() const; - QString getEventSender(const mtx::events::collections::TimelineEvents &event) const; - mtx::events::EventType getEventType( - const mtx::events::collections::TimelineEvents &event) const; template TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); diff --git a/src/Cache.cc b/src/Cache.cc index 92c86322..8b00a828 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -48,6 +48,7 @@ static constexpr const char *MEDIA_DB = "media"; static constexpr const char *SYNC_STATE_DB = "sync_state"; //! Read receipts per room/event. static constexpr const char *READ_RECEIPTS_DB = "read_receipts"; +static constexpr const char *NOTIFICATIONS_DB = "sent_notifications"; using CachedReceipts = std::multimap>; using Receipts = std::map>; @@ -60,6 +61,7 @@ Cache::Cache(const QString &userId, QObject *parent) , invitesDb_{0} , mediaDb_{0} , readReceiptsDb_{0} + , notificationsDb_{0} , localUserId_{userId} {} @@ -112,12 +114,13 @@ Cache::setup() env_.open(statePath.toStdString().c_str()); } - auto txn = lmdb::txn::begin(env_); - syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); - roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); - invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); - mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE); - readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); + auto txn = lmdb::txn::begin(env_); + syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); + roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); + invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); + mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE); + readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); + notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); txn.commit(); qRegisterMetaType(); @@ -1087,6 +1090,36 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_ return members; } +void +Cache::markSentNotification(const std::string &event_id) +{ + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, notificationsDb_, lmdb::val(event_id), lmdb::val(std::string(""))); + txn.commit(); +} + +void +Cache::removeReadNotification(const std::string &event_id) +{ + auto txn = lmdb::txn::begin(env_); + + lmdb::dbi_del(txn, notificationsDb_, lmdb::val(event_id), nullptr); + + txn.commit(); +} + +bool +Cache::isNotificationSent(const std::string &event_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::val value; + bool res = lmdb::dbi_get(txn, notificationsDb_, lmdb::val(event_id), value); + txn.commit(); + + return res; +} + QHash Cache::DisplayNames; QHash Cache::AvatarUrls; diff --git a/src/ChatPage.cc b/src/ChatPage.cc index ee338c2d..4750e67a 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -36,6 +36,7 @@ #include "TypingDisplay.h" #include "UserInfoWidget.h" #include "UserSettingsPage.h" +#include "Utils.h" #include "dialogs/ReadReceipts.h" #include "timeline/TimelineViewManager.h" @@ -339,6 +340,10 @@ ChatPage::ChatPage(QSharedPointer client, connect(client_.data(), &MatrixClient::redactionFailed, this, [this](const QString &error) { emit showNotification(QString("Message redaction failed: %1").arg(error)); }); + connect(client_.data(), + &MatrixClient::notificationsRetrieved, + this, + &ChatPage::sendDesktopNotifications); showContentTimer_ = new QTimer(this); showContentTimer_->setSingleShot(true); @@ -420,13 +425,20 @@ ChatPage::ChatPage(QSharedPointer client, view_manager_->initialize(rooms); removeLeftRooms(rooms.leave); + bool hasNotifications = false; for (const auto &room : rooms.join) { auto room_id = QString::fromStdString(room.first); updateTypingUsers(room_id, room.second.ephemeral.typing); updateRoomNotificationCount( room_id, room.second.unread_notifications.notification_count); + + if (room.second.unread_notifications.notification_count > 0) + hasNotifications = true; } + + if (hasNotifications) + client_->getNotifications(); }); connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync); @@ -838,3 +850,29 @@ ChatPage::updateRoomNotificationCount(const QString &room_id, uint16_t notificat { room_list_->updateUnreadMessageCount(room_id, notification_count); } + +void +ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) +{ + for (const auto &item : res.notifications) { + const auto event_id = utils::event_id(item.event); + + try { + if (item.read) { + cache_->removeReadNotification(event_id); + continue; + } + + if (!cache_->isNotificationSent(event_id)) { + // TODO: send desktop notification + // qDebug() << "sender" << utils::event_sender(item.event); + // qDebug() << "body" << utils::event_body(item.event); + + // We should only sent one notification per event. + // cache_->markSentNotification(event_id); + } + } catch (const lmdb::error &e) { + qWarning() << e.what(); + } + } +} diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index de930fc3..54756c7c 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -1310,3 +1310,41 @@ MatrixClient::redactEvent(const QString &room_id, const QString &event_id) } }); } + +void +MatrixClient::getNotifications() noexcept +{ + QUrlQuery query; + query.addQueryItem("limit", "5"); + + QUrl endpoint(server_); + endpoint.setQuery(query); + endpoint.setPath(clientApiUrl_ + "/notifications"); + + QNetworkRequest request(QString(endpoint.toEncoded())); + setupAuth(request); + + auto reply = get(request); + connect(reply, &QNetworkReply::finished, this, [reply, this]() { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + auto data = reply->readAll(); + + if (status == 0 || status >= 400) { + try { + mtx::errors::Error res = nlohmann::json::parse(data); + std::cout << nlohmann::json::parse(data).dump(2) << '\n'; + // TODO: Response with an error signal + return; + } catch (const std::exception &) { + } + } + + try { + emit notificationsRetrieved(nlohmann::json::parse(data)); + } catch (const std::exception &e) { + qWarning() << "failed to parse /notifications response" << e.what(); + } + }); +} diff --git a/src/Utils.cc b/src/Utils.cc index d9b06b52..14620145 100644 --- a/src/Utils.cc +++ b/src/Utils.cc @@ -111,3 +111,30 @@ utils::levenshtein_distance(const std::string &s1, const std::string &s2) return *std::min_element(row1.begin(), row1.end()); } + +QString +utils::event_body(const mtx::events::collections::TimelineEvents &event) +{ + using namespace mtx::events; + using namespace mtx::events::msg; + + if (mpark::holds_alternative>(event)) { + return message_body>(event); + } else if (mpark::holds_alternative>(event)) { + return message_body>(event); + } else if (mpark::holds_alternative>(event)) { + return message_body>(event); + } else if (mpark::holds_alternative>(event)) { + return message_body>(event); + } else if (mpark::holds_alternative>(event)) { + return message_body>(event); + } else if (mpark::holds_alternative(event)) { + return message_body(event); + } else if (mpark::holds_alternative>(event)) { + return message_body>(event); + } else if (mpark::holds_alternative>(event)) { + return message_body>(event); + } + + return QString(); +} diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 8781f90e..5b433674 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -727,12 +727,6 @@ TimelineView::event(QEvent *event) return QWidget::event(event); } -QString -TimelineView::getEventSender(const mtx::events::collections::TimelineEvents &event) const -{ - return mpark::visit([](auto msg) { return QString::fromStdString(msg.sender); }, event); -} - void TimelineView::toggleScrollDownButton() { @@ -826,8 +820,8 @@ TimelineView::relativeWidget(TimelineItem *item, int dt) const TimelineEvent TimelineView::findFirstViewableEvent(const std::vector &events) { - auto it = std::find_if(events.begin(), events.end(), [this](const auto &event) { - return mtx::events::EventType::RoomMessage == getEventType(event); + 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; @@ -836,19 +830,13 @@ TimelineView::findFirstViewableEvent(const std::vector &events) TimelineEvent TimelineView::findLastViewableEvent(const std::vector &events) { - auto it = std::find_if(events.rbegin(), events.rend(), [this](const auto &event) { - return mtx::events::EventType::RoomMessage == getEventType(event); + auto it = std::find_if(events.rbegin(), events.rend(), [](const auto &event) { + return mtx::events::EventType::RoomMessage == utils::event_type(event); }); return (it == std::rend(events)) ? events.back() : *it; } -inline mtx::events::EventType -TimelineView::getEventType(const mtx::events::collections::TimelineEvents &event) const -{ - return mpark::visit([](auto msg) { return msg.type; }, event); -} - void TimelineView::saveMessageInfo(const QString &sender, uint64_t origin_server_ts,