File indexing completed on 2024-10-06 12:54:07

0001 // SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
0002 // SPDX-License-Identifier: GPL-2.0-or-later
0003 
0004 #include "notificationsmanager.h"
0005 
0006 #include <memory>
0007 
0008 #include <QGuiApplication>
0009 
0010 #include <KLocalizedString>
0011 #include <KNotification>
0012 #include <KNotificationReplyAction>
0013 
0014 #include <QPainter>
0015 #include <Quotient/accountregistry.h>
0016 #include <Quotient/connection.h>
0017 #include <Quotient/csapi/pushrules.h>
0018 #include <Quotient/user.h>
0019 
0020 #include "controller.h"
0021 #include "neochatconfig.h"
0022 #include "neochatroom.h"
0023 #include "roommanager.h"
0024 #include "texthandler.h"
0025 #include "windowcontroller.h"
0026 
0027 using namespace Quotient;
0028 
0029 NotificationsManager &NotificationsManager::instance()
0030 {
0031     static NotificationsManager _instance;
0032     return _instance;
0033 }
0034 
0035 NotificationsManager::NotificationsManager(QObject *parent)
0036     : QObject(parent)
0037 {
0038 }
0039 
0040 void NotificationsManager::handleNotifications(QPointer<Connection> connection)
0041 {
0042     if (!m_connActiveJob.contains(connection->user()->id())) {
0043         auto job = connection->callApi<GetNotificationsJob>();
0044         m_connActiveJob.append(connection->user()->id());
0045         connect(job, &BaseJob::success, this, [this, job, connection]() {
0046             m_connActiveJob.removeAll(connection->user()->id());
0047             processNotificationJob(connection, job, !m_oldNotifications.contains(connection->user()->id()));
0048         });
0049     }
0050 }
0051 
0052 void NotificationsManager::processNotificationJob(QPointer<Quotient::Connection> connection, Quotient::GetNotificationsJob *job, bool initialization)
0053 {
0054     if (job == nullptr) {
0055         return;
0056     }
0057     if (connection == nullptr) {
0058         qWarning() << QStringLiteral("No connection for GetNotificationsJob %1").arg(job->objectName());
0059         return;
0060     }
0061 
0062     const auto connectionId = connection->user()->id();
0063 
0064     // If pagination has occurred set off the next job
0065     auto nextToken = job->jsonData()["next_token"].toString();
0066     if (!nextToken.isEmpty()) {
0067         auto nextJob = connection->callApi<GetNotificationsJob>(nextToken);
0068         m_connActiveJob.append(connectionId);
0069         connect(nextJob, &BaseJob::success, this, [this, nextJob, connection, initialization]() {
0070             m_connActiveJob.removeAll(connection->user()->id());
0071             processNotificationJob(connection, nextJob, initialization);
0072         });
0073     }
0074 
0075     const auto notifications = job->jsonData()["notifications"].toArray();
0076     if (initialization) {
0077         m_oldNotifications[connectionId] = QStringList();
0078         for (const auto &n : notifications) {
0079             if (!m_initialTimestamp.contains(connectionId)) {
0080                 m_initialTimestamp[connectionId] = n.toObject()["ts"].toDouble();
0081             } else {
0082                 qint64 timestamp = n.toObject()["ts"].toDouble();
0083                 if (timestamp > m_initialTimestamp[connectionId]) {
0084                     m_initialTimestamp[connectionId] = timestamp;
0085                 }
0086             }
0087 
0088             auto connectionNotifications = m_oldNotifications.value(connectionId);
0089             connectionNotifications += n.toObject()["event"].toObject()["event_id"].toString();
0090             m_oldNotifications[connectionId] = connectionNotifications;
0091         }
0092         return;
0093     }
0094     for (const auto &n : notifications) {
0095         const auto notification = n.toObject();
0096         if (notification["read"].toBool()) {
0097             continue;
0098         }
0099         auto connectionNotifications = m_oldNotifications.value(connectionId);
0100         if (connectionNotifications.contains(notification["event"].toObject()["event_id"].toString())) {
0101             continue;
0102         }
0103         connectionNotifications += notification["event"].toObject()["event_id"].toString();
0104         m_oldNotifications[connectionId] = connectionNotifications;
0105 
0106         auto room = connection->room(notification["room_id"].toString());
0107         if (shouldPostNotification(connection, n)) {
0108             // The room might have been deleted (for example rejected invitation).
0109             auto sender = room->user(notification["event"].toObject()["sender"].toString());
0110 
0111             QString body;
0112 
0113             if (notification["event"].toObject()["type"].toString() == "org.matrix.msc3381.poll.start") {
0114                 body = notification["event"]
0115                            .toObject()["content"]
0116                            .toObject()["org.matrix.msc3381.poll.start"]
0117                            .toObject()["question"]
0118                            .toObject()["body"]
0119                            .toString();
0120             } else {
0121                 body = notification["event"].toObject()["content"].toObject()["body"].toString();
0122             }
0123 
0124             if (notification["event"]["type"] == "m.room.encrypted") {
0125 #ifdef Quotient_E2EE_ENABLED
0126                 auto decrypted = connection->decryptNotification(notification);
0127                 body = decrypted["content"].toObject()["body"].toString();
0128 #endif
0129                 if (body.isEmpty()) {
0130                     body = i18n("Encrypted Message");
0131                 }
0132             }
0133 
0134             QImage avatar_image;
0135             if (!sender->avatarUrl(room).isEmpty()) {
0136                 avatar_image = sender->avatar(128, room);
0137             } else {
0138                 avatar_image = room->avatar(128);
0139             }
0140             postNotification(dynamic_cast<NeoChatRoom *>(room),
0141                              sender->displayname(room),
0142                              body,
0143                              avatar_image,
0144                              notification["event"].toObject()["event_id"].toString(),
0145                              true);
0146         }
0147     }
0148 }
0149 
0150 bool NotificationsManager::shouldPostNotification(QPointer<Quotient::Connection> connection, const QJsonValue &notification)
0151 {
0152     if (connection == nullptr) {
0153         return false;
0154     }
0155 
0156     auto room = connection->room(notification["room_id"].toString());
0157     if (room == nullptr) {
0158         return false;
0159     }
0160 
0161     // If the room is the current room and the application is active the notification
0162     // should not be shown.
0163     // This is setup so that if the application is inactive the notification will
0164     // always be posted, even if the room is the current room.
0165     bool isCurrentRoom = RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id();
0166     if (isCurrentRoom && QGuiApplication::applicationState() == Qt::ApplicationActive) {
0167         return false;
0168     }
0169 
0170     // If the notification timestamp is earlier than the initial timestamp assume
0171     // the notification is old and shouldn't be posted.
0172     qint64 timestamp = notification["ts"].toDouble();
0173     if (timestamp < m_initialTimestamp[connection->user()->id()]) {
0174         return false;
0175     }
0176 
0177     return true;
0178 }
0179 
0180 void NotificationsManager::postNotification(NeoChatRoom *room,
0181                                             const QString &sender,
0182                                             const QString &text,
0183                                             const QImage &icon,
0184                                             const QString &replyEventId,
0185                                             bool canReply)
0186 {
0187     const QString roomId = room->id();
0188     KNotification *notification = m_notifications.value(roomId);
0189     if (!notification) {
0190         notification = new KNotification("message");
0191         m_notifications.insert(roomId, notification);
0192         connect(notification, &KNotification::closed, this, [this, roomId] {
0193             m_notifications.remove(roomId);
0194         });
0195     }
0196 
0197     QString entry;
0198     if (sender == room->displayName()) {
0199         notification->setTitle(sender);
0200         entry = text.toHtmlEscaped();
0201     } else {
0202         notification->setTitle(room->displayName());
0203         entry = i18n("%1: %2", sender, text.toHtmlEscaped());
0204     }
0205 
0206     notification->setText(notification->text() + '\n' + entry);
0207     notification->setPixmap(createNotificationImage(icon, room));
0208 
0209     notification->setDefaultAction(i18n("Open NeoChat in this room"));
0210     connect(notification, &KNotification::defaultActivated, this, [notification, room]() {
0211         WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
0212         if (!room) {
0213             return;
0214         }
0215         if (room->localUser()->id() != Controller::instance().activeConnection()->userId()) {
0216             Controller::instance().setActiveConnection(Controller::instance().accounts().get(room->localUser()->id()));
0217         }
0218         RoomManager::instance().enterRoom(room);
0219     });
0220 
0221     if (canReply) {
0222         std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
0223         replyAction->setPlaceholderText(i18n("Reply..."));
0224         connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
0225             TextHandler textHandler;
0226             textHandler.setData(text);
0227             room->postMessage(text, textHandler.handleSendText(), RoomMessageEvent::MsgType::Text, replyEventId, QString());
0228         });
0229         notification->setReplyAction(std::move(replyAction));
0230     }
0231 
0232     notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id());
0233     notification->sendEvent();
0234 }
0235 
0236 void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QString &title, const QString &sender, const QImage &icon)
0237 {
0238     QPixmap img;
0239     img.convertFromImage(icon);
0240     KNotification *notification = new KNotification("invite");
0241     notification->setText(i18n("%1 invited you to a room", sender));
0242     notification->setTitle(title);
0243     notification->setPixmap(createNotificationImage(icon, nullptr));
0244     notification->setFlags(KNotification::Persistent);
0245     notification->setDefaultAction(i18n("Open this invitation in NeoChat"));
0246     connect(notification, &KNotification::defaultActivated, this, [notification, room]() {
0247         WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
0248         notification->close();
0249         RoomManager::instance().enterRoom(room);
0250     });
0251     notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")});
0252     connect(notification, &KNotification::action1Activated, this, [room, notification]() {
0253         if (!room) {
0254             return;
0255         }
0256         room->acceptInvitation();
0257         notification->close();
0258     });
0259     connect(notification, &KNotification::action2Activated, this, [room, notification]() {
0260         if (!room) {
0261             return;
0262         }
0263         RoomManager::instance().leaveRoom(room);
0264         notification->close();
0265     });
0266     connect(notification, &KNotification::closed, this, [this, room]() {
0267         if (!room) {
0268             return;
0269         }
0270         m_invitations.remove(room->id());
0271     });
0272 
0273     notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id());
0274 
0275     notification->sendEvent();
0276     m_invitations.insert(room->id(), notification);
0277 }
0278 
0279 void NotificationsManager::clearInvitationNotification(const QString &roomId)
0280 {
0281     if (m_invitations.contains(roomId)) {
0282         m_invitations[roomId]->close();
0283     }
0284 }
0285 
0286 QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
0287 {
0288     // Handle avatars that are lopsided in one dimension
0289     const int biggestDimension = std::max(icon.width(), icon.height());
0290     const QRect imageRect{0, 0, biggestDimension, biggestDimension};
0291 
0292     QImage roundedImage(imageRect.size(), QImage::Format_ARGB32);
0293     roundedImage.fill(Qt::transparent);
0294 
0295     QPainter painter(&roundedImage);
0296     painter.setRenderHint(QPainter::SmoothPixmapTransform);
0297     painter.setPen(Qt::NoPen);
0298 
0299     // Fill background for transparent avatars
0300     painter.setBrush(Qt::white);
0301     painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
0302 
0303     QBrush brush(icon.scaledToHeight(biggestDimension));
0304     painter.setBrush(brush);
0305     painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
0306 
0307     if (room != nullptr) {
0308         const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
0309         if (icon != roomAvatar) {
0310             const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
0311 
0312             painter.setBrush(Qt::white);
0313             painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
0314 
0315             painter.setBrush(roomAvatar.scaled(lowerQuarter.size()));
0316             painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
0317         }
0318     }
0319 
0320     return QPixmap::fromImage(roundedImage);
0321 }
0322 
0323 #include "moc_notificationsmanager.cpp"