Improve the handling of notifications
The aim is to put some additional filtering in place to better stop floods of old notifications. This is achieved with a couple of new filters and better tracking of old notifications. - Make sure to paginate through all notification on initialization to ensure they are all added to old notifications. While we were not previously putting a limit on the number of returned notifications the server can and will do this when there are a very large amount. - Find the newest timestamp for each connection on initialization and don't post any notifications with an earlier timestamp. - Track old notifications on a per-connection basis. Closes network/neochat#358 and network/neochat#423
This commit is contained in:
@@ -45,7 +45,6 @@
|
|||||||
#include <qt_connection_util.h>
|
#include <qt_connection_util.h>
|
||||||
|
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
#include <csapi/notifications.h>
|
|
||||||
#include <eventstats.h>
|
#include <eventstats.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -120,8 +119,8 @@ Controller::Controller(QObject *parent)
|
|||||||
connect(&Accounts, &AccountRegistry::accountCountChanged, this, [this]() {
|
connect(&Accounts, &AccountRegistry::accountCountChanged, this, [this]() {
|
||||||
if (Accounts.size() > oldAccountCount) {
|
if (Accounts.size() > oldAccountCount) {
|
||||||
auto connection = Accounts.accounts()[Accounts.size() - 1];
|
auto connection = Accounts.accounts()[Accounts.size() - 1];
|
||||||
connect(connection, &Connection::syncDone, this, [this, connection]() {
|
connect(connection, &Connection::syncDone, this, [connection]() {
|
||||||
handleNotifications(connection);
|
NotificationsManager::instance().handleNotifications(connection);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
oldAccountCount = Accounts.size();
|
oldAccountCount = Accounts.size();
|
||||||
@@ -129,81 +128,6 @@ Controller::Controller(QObject *parent)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef QUOTIENT_07
|
|
||||||
void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
|
|
||||||
{
|
|
||||||
static QStringList initial;
|
|
||||||
static QStringList oldNotifications;
|
|
||||||
auto job = connection->callApi<GetNotificationsJob>();
|
|
||||||
|
|
||||||
connect(job, &BaseJob::success, this, [job, connection]() {
|
|
||||||
const auto notifications = job->jsonData()["notifications"].toArray();
|
|
||||||
if (!initial.contains(connection->user()->id())) {
|
|
||||||
initial.append(connection->user()->id());
|
|
||||||
for (const auto &n : notifications) {
|
|
||||||
oldNotifications += n.toObject()["event"].toObject()["event_id"].toString();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const auto &n : notifications) {
|
|
||||||
const auto notification = n.toObject();
|
|
||||||
if (notification["read"].toBool()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (oldNotifications.contains(notification["event"].toObject()["event_id"].toString())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
oldNotifications += notification["event"].toObject()["event_id"].toString();
|
|
||||||
auto room = connection->room(notification["room_id"].toString());
|
|
||||||
|
|
||||||
// If room exists, room is NOT active OR the application is NOT active, show notification
|
|
||||||
if (room
|
|
||||||
&& !(RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id()
|
|
||||||
&& QGuiApplication::applicationState() == Qt::ApplicationActive)) {
|
|
||||||
// The room might have been deleted (for example rejected invitation).
|
|
||||||
auto sender = room->user(notification["event"].toObject()["sender"].toString());
|
|
||||||
|
|
||||||
QString body;
|
|
||||||
|
|
||||||
if (notification["event"].toObject()["type"].toString() == "org.matrix.msc3381.poll.start") {
|
|
||||||
body = notification["event"]
|
|
||||||
.toObject()["content"]
|
|
||||||
.toObject()["org.matrix.msc3381.poll.start"]
|
|
||||||
.toObject()["question"]
|
|
||||||
.toObject()["body"]
|
|
||||||
.toString();
|
|
||||||
} else {
|
|
||||||
body = notification["event"].toObject()["content"].toObject()["body"].toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification["event"]["type"] == "m.room.encrypted") {
|
|
||||||
#ifdef Quotient_E2EE_ENABLED
|
|
||||||
auto decrypted = connection->decryptNotification(notification);
|
|
||||||
body = decrypted["content"].toObject()["body"].toString();
|
|
||||||
#endif
|
|
||||||
if (body.isEmpty()) {
|
|
||||||
body = i18n("Encrypted Message");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QImage avatar_image;
|
|
||||||
if (!sender->avatarUrl(room).isEmpty()) {
|
|
||||||
avatar_image = sender->avatar(128, room);
|
|
||||||
} else {
|
|
||||||
avatar_image = room->avatar(128);
|
|
||||||
}
|
|
||||||
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
|
|
||||||
sender->displayname(room),
|
|
||||||
body,
|
|
||||||
avatar_image,
|
|
||||||
notification["event"].toObject()["event_id"].toString(),
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Controller &Controller::instance()
|
Controller &Controller::instance()
|
||||||
{
|
{
|
||||||
static Controller _instance;
|
static Controller _instance;
|
||||||
|
|||||||
@@ -230,9 +230,6 @@ private:
|
|||||||
QMap<Quotient::Room *, int> m_notificationCounts;
|
QMap<Quotient::Room *, int> m_notificationCounts;
|
||||||
|
|
||||||
bool hasWindowSystem() const;
|
bool hasWindowSystem() const;
|
||||||
#ifdef QUOTIENT_07
|
|
||||||
void handleNotifications(QPointer<Quotient::Connection> connection);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void invokeLogin();
|
void invokeLogin();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include <QJsonArray>
|
#include <QGuiApplication>
|
||||||
|
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
#include <KNotification>
|
#include <KNotification>
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <connection.h>
|
#include <connection.h>
|
||||||
|
#include <csapi/notifications.h>
|
||||||
#include <csapi/pushrules.h>
|
#include <csapi/pushrules.h>
|
||||||
#include <jobs/basejob.h>
|
#include <jobs/basejob.h>
|
||||||
#include <user.h>
|
#include <user.h>
|
||||||
@@ -48,6 +49,148 @@ NotificationsManager::NotificationsManager(QObject *parent)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
void NotificationsManager::handleNotifications(QPointer<Connection> connection)
|
||||||
|
{
|
||||||
|
if (!m_connActiveJob.contains(connection->user()->id())) {
|
||||||
|
auto job = connection->callApi<GetNotificationsJob>();
|
||||||
|
m_connActiveJob.append(connection->user()->id());
|
||||||
|
connect(job, &BaseJob::success, this, [this, job, connection]() {
|
||||||
|
m_connActiveJob.removeAll(connection->user()->id());
|
||||||
|
processNotificationJob(connection, job, !m_oldNotifications.contains(connection->user()->id()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void NotificationsManager::processNotificationJob(QPointer<Quotient::Connection> connection, Quotient::GetNotificationsJob *job, bool initialization)
|
||||||
|
{
|
||||||
|
if (job == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (connection == nullptr) {
|
||||||
|
qWarning() << QStringLiteral("No connection for GetNotificationsJob %1").arg(job->objectName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto connectionId = connection->user()->id();
|
||||||
|
|
||||||
|
// If pagination has occurred set off the next job
|
||||||
|
auto nextToken = job->jsonData()["next_token"].toString();
|
||||||
|
if (!nextToken.isEmpty()) {
|
||||||
|
auto nextJob = connection->callApi<GetNotificationsJob>(nextToken);
|
||||||
|
m_connActiveJob.append(connectionId);
|
||||||
|
connect(nextJob, &BaseJob::success, this, [this, nextJob, connection, initialization]() {
|
||||||
|
m_connActiveJob.removeAll(connection->user()->id());
|
||||||
|
processNotificationJob(connection, nextJob, initialization);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto notifications = job->jsonData()["notifications"].toArray();
|
||||||
|
if (initialization) {
|
||||||
|
m_oldNotifications[connectionId] = QStringList();
|
||||||
|
for (const auto &n : notifications) {
|
||||||
|
if (!m_initialTimestamp.contains(connectionId)) {
|
||||||
|
m_initialTimestamp[connectionId] = n.toObject()["ts"].toDouble();
|
||||||
|
} else {
|
||||||
|
qint64 timestamp = n.toObject()["ts"].toDouble();
|
||||||
|
if (timestamp > m_initialTimestamp[connectionId]) {
|
||||||
|
m_initialTimestamp[connectionId] = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto connectionNotifications = m_oldNotifications.value(connectionId);
|
||||||
|
connectionNotifications += n.toObject()["event"].toObject()["event_id"].toString();
|
||||||
|
m_oldNotifications[connectionId] = connectionNotifications;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto &n : notifications) {
|
||||||
|
const auto notification = n.toObject();
|
||||||
|
if (notification["read"].toBool()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto connectionNotifications = m_oldNotifications.value(connectionId);
|
||||||
|
if (connectionNotifications.contains(notification["event"].toObject()["event_id"].toString())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
connectionNotifications += notification["event"].toObject()["event_id"].toString();
|
||||||
|
m_oldNotifications[connectionId] = connectionNotifications;
|
||||||
|
|
||||||
|
auto room = connection->room(notification["room_id"].toString());
|
||||||
|
if (shouldPostNotification(connection, n)) {
|
||||||
|
// The room might have been deleted (for example rejected invitation).
|
||||||
|
auto sender = room->user(notification["event"].toObject()["sender"].toString());
|
||||||
|
|
||||||
|
QString body;
|
||||||
|
|
||||||
|
if (notification["event"].toObject()["type"].toString() == "org.matrix.msc3381.poll.start") {
|
||||||
|
body = notification["event"]
|
||||||
|
.toObject()["content"]
|
||||||
|
.toObject()["org.matrix.msc3381.poll.start"]
|
||||||
|
.toObject()["question"]
|
||||||
|
.toObject()["body"]
|
||||||
|
.toString();
|
||||||
|
} else {
|
||||||
|
body = notification["event"].toObject()["content"].toObject()["body"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification["event"]["type"] == "m.room.encrypted") {
|
||||||
|
#ifdef Quotient_E2EE_ENABLED
|
||||||
|
auto decrypted = connection->decryptNotification(notification);
|
||||||
|
body = decrypted["content"].toObject()["body"].toString();
|
||||||
|
#endif
|
||||||
|
if (body.isEmpty()) {
|
||||||
|
body = i18n("Encrypted Message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage avatar_image;
|
||||||
|
if (!sender->avatarUrl(room).isEmpty()) {
|
||||||
|
avatar_image = sender->avatar(128, room);
|
||||||
|
} else {
|
||||||
|
avatar_image = room->avatar(128);
|
||||||
|
}
|
||||||
|
postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||||
|
sender->displayname(room),
|
||||||
|
body,
|
||||||
|
avatar_image,
|
||||||
|
notification["event"].toObject()["event_id"].toString(),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NotificationsManager::shouldPostNotification(QPointer<Quotient::Connection> connection, const QJsonValue ¬ification)
|
||||||
|
{
|
||||||
|
if (connection == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto room = connection->room(notification["room_id"].toString());
|
||||||
|
if (room == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the room is the current room and the application is active the notification
|
||||||
|
// should not be shown.
|
||||||
|
// This is setup so that if the application is inactive the notification will
|
||||||
|
// always be posted, even if the room is the current room.
|
||||||
|
bool isCurrentRoom = RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id();
|
||||||
|
if (isCurrentRoom && QGuiApplication::applicationState() == Qt::ApplicationActive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the notification timestamp is earlier than the initial timestamp assume
|
||||||
|
// the notification is old and shouldn't be posted.
|
||||||
|
qint64 timestamp = notification["ts"].toDouble();
|
||||||
|
if (timestamp < m_initialTimestamp[connection->user()->id()]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void NotificationsManager::postNotification(NeoChatRoom *room,
|
void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||||
const QString &sender,
|
const QString &sender,
|
||||||
const QString &text,
|
const QString &text,
|
||||||
|
|||||||
@@ -4,11 +4,18 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QJsonObject>
|
#include <csapi/notifications.h>
|
||||||
|
#include <jobs/basejob.h>
|
||||||
|
|
||||||
|
namespace Quotient
|
||||||
|
{
|
||||||
|
class Connection;
|
||||||
|
}
|
||||||
|
|
||||||
class KNotification;
|
class KNotification;
|
||||||
class NeoChatRoom;
|
class NeoChatRoom;
|
||||||
@@ -181,9 +188,23 @@ public:
|
|||||||
*/
|
*/
|
||||||
QVector<QVariant> getKeywordNotificationActions();
|
QVector<QVariant> getKeywordNotificationActions();
|
||||||
|
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
/**
|
||||||
|
* @brief Handle the notifications for the given connection.
|
||||||
|
*/
|
||||||
|
void handleNotifications(QPointer<Quotient::Connection> connection);
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
NotificationsManager(QObject *parent = nullptr);
|
NotificationsManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QHash<QString, qint64> m_initialTimestamp;
|
||||||
|
QHash<QString, QStringList> m_oldNotifications;
|
||||||
|
|
||||||
|
QStringList m_connActiveJob;
|
||||||
|
|
||||||
|
bool shouldPostNotification(QPointer<Quotient::Connection> connection, const QJsonValue ¬ification);
|
||||||
|
|
||||||
QHash<QString, KNotification *> m_notifications;
|
QHash<QString, KNotification *> m_notifications;
|
||||||
QHash<QString, QPointer<KNotification>> m_invitations;
|
QHash<QString, QPointer<KNotification>> m_invitations;
|
||||||
|
|
||||||
@@ -218,6 +239,8 @@ private:
|
|||||||
QVector<QVariant> toActions(PushNotificationAction::Action action, const QString &sound = "default");
|
QVector<QVariant> toActions(PushNotificationAction::Action action, const QString &sound = "default");
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
|
void processNotificationJob(QPointer<Quotient::Connection> connection, Quotient::GetNotificationsJob *job, bool initialization);
|
||||||
|
|
||||||
void updateNotificationRules(const QString &type);
|
void updateNotificationRules(const QString &type);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
|
|||||||
Reference in New Issue
Block a user