diff --git a/src/Cache.cpp b/src/Cache.cpp index e841a9dc..863f0683 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -1518,10 +1518,6 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei // Append the new ones. for (const auto &[read_by, timestamp] : event_receipts) { - if (read_by == user_id) { - emit removeNotification(QString::fromStdString(room_id), - QString::fromStdString(event_id)); - } saved_receipts.emplace(read_by, timestamp); } @@ -1818,12 +1814,6 @@ Cache::saveState(const mtx::responses::Sync &res) updatedInfo.tags.push_back(tag.first); } } - if (auto fr = std::get_if< - mtx::events::AccountDataEvent>(&evt)) { - nhlog::db()->debug("Fully read: {}", fr->content.event_id); - emit removeNotification(QString::fromStdString(room.first), - QString::fromStdString(fr->content.event_id)); - } } } @@ -2134,27 +2124,6 @@ Cache::roomIds() return rooms; } -QMap -Cache::getTimelineMentions() -{ - // TODO: Should be read-only, but getMentionsDb will attempt to create a DB - // if it doesn't exist, throwing an error. - auto txn = lmdb::txn::begin(env_, nullptr); - - QMap notifs; - - auto room_ids = getRoomIds(txn); - - for (const auto &room_id : room_ids) { - auto roomNotifs = getTimelineMentionsForRoom(txn, room_id); - notifs[QString::fromStdString(room_id)] = roomNotifs; - } - - txn.commit(); - - return notifs; -} - std::string Cache::previousBatchToken(const std::string &room_id) { @@ -3587,88 +3556,6 @@ Cache::clearTimeline(const std::string &room_id) txn.commit(); } -mtx::responses::Notifications -Cache::getTimelineMentionsForRoom(lmdb::txn &txn, const std::string &room_id) -{ - auto db = getMentionsDb(txn, room_id); - - if (db.size(txn) == 0) { - return mtx::responses::Notifications{}; - } - - mtx::responses::Notifications notif; - std::string_view event_id, msg; - - auto cursor = lmdb::cursor::open(txn, db); - - while (cursor.get(event_id, msg, MDB_NEXT)) { - auto obj = nlohmann::json::parse(msg); - - if (obj.count("event") == 0) - continue; - - mtx::responses::Notification notification; - from_json(obj, notification); - - notif.notifications.push_back(notification); - } - cursor.close(); - - std::reverse(notif.notifications.begin(), notif.notifications.end()); - - return notif; -} - -//! Add all notifications containing a user mention to the db. -void -Cache::saveTimelineMentions(const mtx::responses::Notifications &res) -{ - QMap> notifsByRoom; - - // Sort into room-specific 'buckets' - for (const auto ¬if : res.notifications) { - nlohmann::json val = notif; - notifsByRoom[notif.room_id].push_back(notif); - } - - auto txn = lmdb::txn::begin(env_); - // Insert the entire set of mentions for each room at a time. - QMap>::const_iterator it = - notifsByRoom.constBegin(); - auto end = notifsByRoom.constEnd(); - while (it != end) { - nhlog::db()->debug("Storing notifications for " + it.key()); - saveTimelineMentions(txn, it.key(), std::move(it.value())); - ++it; - } - - txn.commit(); -} - -void -Cache::saveTimelineMentions(lmdb::txn &txn, - const std::string &room_id, - const QList &res) -{ - auto db = getMentionsDb(txn, room_id); - - using namespace mtx::events; - using namespace mtx::events::state; - - for (const auto ¬if : res) { - const auto event_id = mtx::accessors::event_id(notif.event); - - // double check that we have the correct room_id... - if (room_id.compare(notif.room_id) != 0) { - return; - } - - nlohmann::json obj = notif; - - db.put(txn, event_id, obj.dump()); - } -} - void Cache::markSentNotification(const std::string &event_id) { @@ -5299,12 +5186,6 @@ roomIds() return instance_->roomIds(); } -QMap -getTimelineMentions() -{ - return instance_->getTimelineMentions(); -} - //! Retrieve all the user ids from a room. std::vector roomMembers(const std::string &room_id) @@ -5405,13 +5286,6 @@ isNotificationSent(const std::string &event_id) return instance_->isNotificationSent(event_id); } -//! Add all notifications containing a user mention to the db. -void -saveTimelineMentions(const mtx::responses::Notifications &res) -{ - instance_->saveTimelineMentions(res); -} - //! Remove old unused data. void deleteOldMessages() diff --git a/src/Cache.h b/src/Cache.h index 7ea659ec..bb779866 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -118,9 +118,6 @@ setCurrentFormat(); bool runMigrations(); -QMap -getTimelineMentions(); - //! Retrieve all the user ids from a room. std::vector roomMembers(const std::string &room_id); @@ -178,10 +175,6 @@ removeReadNotification(const std::string &event_id); bool isNotificationSent(const std::string &event_id); -//! Add all notifications containing a user mention to the db. -void -saveTimelineMentions(const mtx::responses::Notifications &res); - //! Remove old unused data. void deleteOldMessages(); diff --git a/src/Cache_p.h b/src/Cache_p.h index 0c75acdb..1694adb7 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -138,7 +138,6 @@ public: bool runMigrations(); std::vector roomIds(); - QMap getTimelineMentions(); //! Retrieve all the user ids from a room. std::vector roomMembers(const std::string &room_id); @@ -179,9 +178,6 @@ public: //! Check if we have sent a desktop notification for the given event id. bool isNotificationSent(const std::string &event_id); - //! Add all notifications containing a user mention to the db. - void saveTimelineMentions(const mtx::responses::Notifications &res); - //! retrieve events in timeline and related functions struct Messages { @@ -318,7 +314,6 @@ public: signals: void newReadReceipts(const QString &room_id, const std::vector &event_ids); void roomReadStatus(const std::map &status); - void removeNotification(const QString &room_id, const QString &event_id); void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); void userKeysUpdateFinalize(const std::string &user_id); void verificationStatusChanged(const std::string &userid); @@ -335,15 +330,6 @@ private: lmdb::dbi &membersdb, const mtx::responses::InvitedRoom &room); - //! Add a notification containing a user mention to the db. - void saveTimelineMentions(lmdb::txn &txn, - const std::string &room_id, - const QList &res); - - //! Get timeline items that a user was mentions in for a given room - mtx::responses::Notifications - getTimelineMentionsForRoom(lmdb::txn &txn, const std::string &room_id); - QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); @@ -642,11 +628,6 @@ private: return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); } - lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); - } - lmdb::dbi getUserKeysDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "user_key", MDB_CREATE); } lmdb::dbi getVerificationDb(lmdb::txn &txn) diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index d6b3d292..1f38d763 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -136,18 +136,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QObject *parent) Qt::QueuedConnection); connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); connect(this, &ChatPage::changeToRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection); - connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications); - connect(this, - &ChatPage::highlightedNotifsRetrieved, - this, - [](const mtx::responses::Notifications ¬if) { - try { - cache::saveTimelineMentions(notif); - } catch (const lmdb::error &e) { - nhlog::db()->error("failed to save mentions: {}", e.what()); - } - }); - connect(notificationsManager, &NotificationsManager::notificationClicked, this, @@ -208,18 +196,140 @@ ChatPage::ChatPage(QSharedPointer userSettings, QObject *parent) // No need to check amounts for this section, as this function internally checks for // duplicates. if (notificationCount && userSettings_->hasNotifications()) - http::client()->notifications( - 5, - "", - "", - [this](const mtx::responses::Notifications &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve notifications: {}", err); - return; - } + for (const auto &e : sync.account_data.events) { + if (auto newRules = + std::get_if>(&e)) + pushrules = + std::make_unique(newRules->content.global); + } + if (!pushrules) { + auto eventInDb = cache::client()->getAccountData(mtx::events::EventType::PushRules); + if (eventInDb) { + if (auto newRules = + std::get_if>( + &*eventInDb)) + pushrules = + std::make_unique(newRules->content.global); + } + } + if (pushrules) { + const auto local_user = utils::localUser().toStdString(); - emit notificationsRetrieved(std::move(res)); - }); + for (const auto &[room_id, room] : sync.rooms.join) { + // clear old notifications + for (const auto &e : room.ephemeral.events) { + if (auto receiptsEv = + std::get_if>( + &e)) { + std::vector receipts; + + for (const auto &[event_id, userReceipts] : receiptsEv->content.receipts) { + if (auto r = userReceipts.find(mtx::events::ephemeral::Receipt::Read); + r != userReceipts.end()) { + for (const auto &[user_id, receipt] : r->second.users) { + (void)receipt; + + if (user_id == local_user) { + receipts.push_back(QString::fromStdString(event_id)); + break; + } + } + } + if (auto r = + userReceipts.find(mtx::events::ephemeral::Receipt::ReadPrivate); + r != userReceipts.end()) { + for (const auto &[user_id, receipt] : r->second.users) { + (void)receipt; + + if (user_id == local_user) { + receipts.push_back(QString::fromStdString(event_id)); + break; + } + } + } + } + if (!receipts.empty()) + notificationsManager->removeNotifications( + QString::fromStdString(room_id), receipts); + } + } + + // calculate new notifications + if (!room.timeline.events.empty() && + (room.unread_notifications.notification_count || + room.unread_notifications.highlight_count)) { + auto roomModel = + view_manager_->rooms()->getRoomById(QString::fromStdString(room_id)); + + if (!roomModel) { + continue; + } + + auto currentReadMarker = + cache::getEventIndex(room_id, cache::client()->getFullyReadEventId(room_id)); + + auto ctx = roomModel->pushrulesRoomContext(); + for (const auto &event : room.timeline.events) { + mtx::events::collections::TimelineEvent te{event}; + std::visit([room_id = room_id](auto &event_) { event_.room_id = room_id; }, + te.data); + + if (auto encryptedEvent = + std::get_if>( + &event)) { + MegolmSessionIndex index(room_id, encryptedEvent->content); + + auto result = olm::decryptEvent(index, *encryptedEvent); + if (result.event) + te.data = result.event.value(); + } + + auto actions = pushrules->evaluate(te, ctx); + if (std::find(actions.begin(), + actions.end(), + mtx::pushrules::actions::Action{ + mtx::pushrules::actions::notify{}}) != actions.end()) { + auto event_id = mtx::accessors::event_id(event); + + // skip already read events + if (currentReadMarker && + currentReadMarker > cache::getEventIndex(room_id, event_id)) + continue; + + if (!cache::isNotificationSent(event_id)) { + // We should only send one notification per event. + cache::markSentNotification(event_id); + + // Don't send a notification when the current room is opened. + if (isRoomActive(roomModel->roomId())) + continue; + + if (userSettings_->hasDesktopNotifications()) { + auto info = cache::singleRoomInfo(room_id); + + AvatarProvider::resolve( + roomModel->roomAvatarUrl(), + 96, + this, + [this, te, room_id = room_id, actions](QPixmap image) { + notificationsManager->postNotification( + mtx::responses::Notification{ + .actions = actions, + .event = te.data, + .read = false, + .profile_tag = "", + .room_id = room_id, + .ts = 0, + }, + image.toImage()); + }); + } + } + } + } + } + } + } }); connect( @@ -394,12 +504,6 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) &Cache::newReadReceipts, view_manager_, &TimelineViewManager::updateReadReceipts); - - connect(cache::client(), - &Cache::removeNotification, - notificationsManager, - &NotificationsManager::removeNotification); - } catch (const lmdb::error &e) { nhlog::db()->critical("failure during boot: {}", e.what()); emit dropToLoginPageCb(tr("Failed to open database, logging out!")); @@ -415,7 +519,6 @@ ChatPage::loadStateFromCache() olm::client()->load(cache::restoreOlmAccount(), cache::client()->pickleSecret()); emit initializeEmptyViews(); - emit initializeMentions(cache::getTimelineMentions()); cache::calculateRoomReadStatus(); @@ -464,46 +567,6 @@ ChatPage::removeRoom(const QString &room_id) } } -void -ChatPage::sendNotifications(const mtx::responses::Notifications &res) -{ - for (const auto &item : res.notifications) { - const auto event_id = mtx::accessors::event_id(item.event); - - try { - if (item.read) { - cache::removeReadNotification(event_id); - continue; - } - - if (!cache::isNotificationSent(event_id)) { - const auto room_id = QString::fromStdString(item.room_id); - - // We should only send one notification per event. - cache::markSentNotification(event_id); - - // Don't send a notification when the current room is opened. - if (isRoomActive(room_id)) - continue; - - if (userSettings_->hasDesktopNotifications()) { - auto info = cache::singleRoomInfo(item.room_id); - - AvatarProvider::resolve(QString::fromStdString(info.avatar_url), - 96, - this, - [this, item](QPixmap image) { - notificationsManager->postNotification( - item, image.toImage()); - }); - } - } - } catch (const lmdb::error &e) { - nhlog::db()->warn("error while sending notification: {}", e.what()); - } - } -} - void ChatPage::tryInitialSync() { @@ -596,7 +659,6 @@ ChatPage::startInitialSync() olm::handle_to_device_messages(res.to_device.events); emit initializeViews(std::move(res)); - emit initializeMentions(cache::getTimelineMentions()); cache::calculateRoomReadStatus(); } catch (const lmdb::error &e) { diff --git a/src/ChatPage.h b/src/ChatPage.h index 73b7efcc..1bb25dc2 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -108,9 +109,6 @@ signals: void connectionLost(); void connectionRestored(); - void notificationsRetrieved(const mtx::responses::Notifications &); - void highlightedNotifsRetrieved(const mtx::responses::Notifications &, const QPoint widgetPos); - void contentLoaded(); void closing(); void changeWindowTitle(const int); @@ -135,7 +133,6 @@ signals: void initializeViews(const mtx::responses::Sync &rooms); void initializeEmptyViews(); - void initializeMentions(const QMap ¬ifs); void syncUI(const mtx::responses::Sync &sync); void dropToLoginPageCb(const QString &msg); @@ -206,9 +203,6 @@ private: template Memberships getMemberships(const std::vector &events) const; - //! Send desktop notification for the received messages. - void sendNotifications(const mtx::responses::Notifications &); - template void connectCallMessage(); @@ -222,6 +216,8 @@ private: NotificationsManager *notificationsManager; CallManager *callManager_; + + std::unique_ptr pushrules; }; template diff --git a/src/notifications/Manager.cpp b/src/notifications/Manager.cpp index 501bfb3f..6033cc6d 100644 --- a/src/notifications/Manager.cpp +++ b/src/notifications/Manager.cpp @@ -7,6 +7,7 @@ #include "Cache.h" #include "EventAccessors.h" +#include "Logging.h" #include "Utils.h" QString @@ -33,3 +34,24 @@ NotificationsManager::getMessageTemplate(const mtx::responses::Notification ¬ return QStringLiteral("%1: %2").arg(sender); } } + +void +NotificationsManager::removeNotifications(const QString &roomId, + const std::vector &eventIds) +{ + std::string room_id = roomId.toStdString(); + + std::uint64_t markerPos = 0; + for (const auto &e : eventIds) { + markerPos = std::max(markerPos, cache::getEventIndex(room_id, e.toStdString()).value_or(0)); + } + + for (const auto &[roomId, eventId] : this->notificationIds) { + if (roomId != roomId) + continue; + auto idx = cache::getEventIndex(room_id, eventId.toStdString()); + if (!idx || markerPos >= idx) { + removeNotification(roomId, eventId); + } + } +} diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h index f3b1fe30..05f06dcf 100644 --- a/src/notifications/Manager.h +++ b/src/notifications/Manager.h @@ -36,6 +36,8 @@ public: void postNotification(const mtx::responses::Notification ¬ification, const QImage &icon); + void removeNotification(const QString &roomId, const QString &eventId); + signals: void notificationClicked(const QString roomId, const QString eventId); void sendNotificationReply(const QString roomId, const QString eventId, const QString body); @@ -46,7 +48,7 @@ signals: const QImage &icon); public slots: - void removeNotification(const QString &roomId, const QString &eventId); + void removeNotifications(const QString &roomId, const std::vector &eventId); #if defined(NHEKO_DBUS_SYS) public: @@ -62,9 +64,6 @@ private: const QImage &icon); void closeNotification(uint id); - // notification ID to (room ID, event ID) - QMap notificationIds; - const bool hasMarkup_; const bool hasImages_; #endif @@ -96,6 +95,10 @@ private slots: private: QString getMessageTemplate(const mtx::responses::Notification ¬ification); + + // notification ID to (room ID, event ID) + // Only populated on Linux atm + QMap notificationIds; }; #if defined(NHEKO_DBUS_SYS) diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h index aa10999d..1b3f55e4 100644 --- a/src/timeline/Permissions.h +++ b/src/timeline/Permissions.h @@ -34,6 +34,8 @@ public: void invalidate(); + const mtx::events::state::PowerLevels &powerlevelEvent() const { return pl; }; + private: QString roomId_; mtx::events::state::PowerLevels pl; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 0eec5cae..0e726bde 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -2921,6 +2921,17 @@ TimelineModel::directChatOtherUserId() const return {}; } +mtx::pushrules::PushRuleEvaluator::RoomContext +TimelineModel::pushrulesRoomContext() const +{ + return mtx::pushrules::PushRuleEvaluator::RoomContext{ + .user_display_name = + cache::displayName(room_id_.toStdString(), http::client()->user_id().to_string()), + .member_count = cache::client()->memberCount(room_id_.toStdString()), + .power_levels = permissions_.powerlevelEvent(), + }; +} + RoomSummary * TimelineModel::parentSpace() { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 8e191556..a4904f4f 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -336,6 +336,8 @@ public: bool isDirect() const { return roomMemberCount() <= 2; } QString directChatOtherUserId() const; + mtx::pushrules::PushRuleEvaluator::RoomContext pushrulesRoomContext() const; + std::optional eventById(const QString &id) { auto e = events.get(id.toStdString(), "");