File indexing completed on 2024-09-15 04:28:34
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/csapi/pushrules.h> 0017 #include <Quotient/user.h> 0018 0019 #ifdef HAVE_KIO 0020 #include <KIO/ApplicationLauncherJob> 0021 #endif 0022 0023 #include "controller.h" 0024 #include "neochatconnection.h" 0025 #include "neochatroom.h" 0026 #include "roommanager.h" 0027 #include "texthandler.h" 0028 #include "windowcontroller.h" 0029 0030 using namespace Quotient; 0031 0032 NotificationsManager &NotificationsManager::instance() 0033 { 0034 static NotificationsManager _instance; 0035 return _instance; 0036 } 0037 0038 NotificationsManager::NotificationsManager(QObject *parent) 0039 : QObject(parent) 0040 { 0041 } 0042 0043 void NotificationsManager::handleNotifications(QPointer<NeoChatConnection> connection) 0044 { 0045 if (!m_connActiveJob.contains(connection->user()->id())) { 0046 auto job = connection->callApi<GetNotificationsJob>(); 0047 m_connActiveJob.append(connection->user()->id()); 0048 connect(job, &BaseJob::success, this, [this, job, connection]() { 0049 m_connActiveJob.removeAll(connection->user()->id()); 0050 processNotificationJob(connection, job, !m_oldNotifications.contains(connection->user()->id())); 0051 }); 0052 } 0053 } 0054 0055 void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization) 0056 { 0057 if (job == nullptr) { 0058 return; 0059 } 0060 if (connection == nullptr) { 0061 qWarning() << QStringLiteral("No connection for GetNotificationsJob %1").arg(job->objectName()); 0062 return; 0063 } 0064 0065 const auto connectionId = connection->user()->id(); 0066 0067 // If pagination has occurred set off the next job 0068 auto nextToken = job->jsonData()["next_token"_ls].toString(); 0069 if (!nextToken.isEmpty()) { 0070 auto nextJob = connection->callApi<GetNotificationsJob>(nextToken); 0071 m_connActiveJob.append(connectionId); 0072 connect(nextJob, &BaseJob::success, this, [this, nextJob, connection, initialization]() { 0073 m_connActiveJob.removeAll(connection->user()->id()); 0074 processNotificationJob(connection, nextJob, initialization); 0075 }); 0076 } 0077 0078 const auto notifications = job->jsonData()["notifications"_ls].toArray(); 0079 if (initialization) { 0080 m_oldNotifications[connectionId] = QStringList(); 0081 for (const auto &n : notifications) { 0082 if (!m_initialTimestamp.contains(connectionId)) { 0083 m_initialTimestamp[connectionId] = n.toObject()["ts"_ls].toDouble(); 0084 } else { 0085 qint64 timestamp = n.toObject()["ts"_ls].toDouble(); 0086 if (timestamp > m_initialTimestamp[connectionId]) { 0087 m_initialTimestamp[connectionId] = timestamp; 0088 } 0089 } 0090 0091 auto connectionNotifications = m_oldNotifications.value(connectionId); 0092 connectionNotifications += n.toObject()["event"_ls].toObject()["event_id"_ls].toString(); 0093 m_oldNotifications[connectionId] = connectionNotifications; 0094 } 0095 return; 0096 } 0097 for (const auto &n : notifications) { 0098 const auto notification = n.toObject(); 0099 if (notification["read"_ls].toBool()) { 0100 continue; 0101 } 0102 auto connectionNotifications = m_oldNotifications.value(connectionId); 0103 if (connectionNotifications.contains(notification["event"_ls].toObject()["event_id"_ls].toString())) { 0104 continue; 0105 } 0106 connectionNotifications += notification["event"_ls].toObject()["event_id"_ls].toString(); 0107 m_oldNotifications[connectionId] = connectionNotifications; 0108 0109 auto room = connection->room(notification["room_id"_ls].toString()); 0110 if (shouldPostNotification(connection, n)) { 0111 // The room might have been deleted (for example rejected invitation). 0112 auto sender = room->user(notification["event"_ls].toObject()["sender"_ls].toString()); 0113 0114 QString body; 0115 0116 if (notification["event"_ls].toObject()["type"_ls].toString() == "org.matrix.msc3381.poll.start"_ls) { 0117 body = notification["event"_ls] 0118 .toObject()["content"_ls] 0119 .toObject()["org.matrix.msc3381.poll.start"_ls] 0120 .toObject()["question"_ls] 0121 .toObject()["body"_ls] 0122 .toString(); 0123 } else { 0124 body = notification["event"_ls].toObject()["content"_ls].toObject()["body"_ls].toString(); 0125 } 0126 0127 if (notification["event"_ls]["type"_ls] == "m.room.encrypted"_ls) { 0128 auto decrypted = connection->decryptNotification(notification); 0129 body = decrypted["content"_ls].toObject()["body"_ls].toString(); 0130 if (body.isEmpty()) { 0131 body = i18n("Encrypted Message"); 0132 } 0133 } 0134 0135 QImage avatar_image; 0136 if (!sender->avatarUrl(room).isEmpty()) { 0137 avatar_image = sender->avatar(128, room); 0138 } else { 0139 avatar_image = room->avatar(128); 0140 } 0141 postNotification(dynamic_cast<NeoChatRoom *>(room), 0142 sender->displayname(room), 0143 body, 0144 avatar_image, 0145 notification["event"_ls].toObject()["event_id"_ls].toString(), 0146 true); 0147 } 0148 } 0149 } 0150 0151 bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue ¬ification) 0152 { 0153 if (connection == nullptr) { 0154 return false; 0155 } 0156 0157 auto room = connection->room(notification["room_id"_ls].toString()); 0158 if (room == nullptr) { 0159 return false; 0160 } 0161 0162 // If the room is the current room and the application is active the notification 0163 // should not be shown. 0164 // This is setup so that if the application is inactive the notification will 0165 // always be posted, even if the room is the current room. 0166 bool isCurrentRoom = RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id(); 0167 if (isCurrentRoom && QGuiApplication::applicationState() == Qt::ApplicationActive) { 0168 return false; 0169 } 0170 0171 // If the notification timestamp is earlier than the initial timestamp assume 0172 // the notification is old and shouldn't be posted. 0173 qint64 timestamp = notification["ts"_ls].toDouble(); 0174 if (timestamp < m_initialTimestamp[connection->user()->id()]) { 0175 return false; 0176 } 0177 0178 return true; 0179 } 0180 0181 void NotificationsManager::postNotification(NeoChatRoom *room, 0182 const QString &sender, 0183 const QString &text, 0184 const QImage &icon, 0185 const QString &replyEventId, 0186 bool canReply) 0187 { 0188 const QString roomId = room->id(); 0189 KNotification *notification = m_notifications.value(roomId); 0190 if (!notification) { 0191 notification = new KNotification(QStringLiteral("message")); 0192 m_notifications.insert(roomId, notification); 0193 connect(notification, &KNotification::closed, this, [this, roomId] { 0194 m_notifications.remove(roomId); 0195 }); 0196 } 0197 0198 QString entry; 0199 if (sender == room->displayName()) { 0200 notification->setTitle(sender); 0201 entry = text.toHtmlEscaped(); 0202 } else { 0203 notification->setTitle(room->displayName()); 0204 entry = i18n("%1: %2", sender, text.toHtmlEscaped()); 0205 } 0206 0207 notification->setText(notification->text() + QLatin1Char('\n') + entry); 0208 notification->setPixmap(createNotificationImage(icon, room)); 0209 0210 auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room")); 0211 connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() { 0212 WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken()); 0213 if (!room) { 0214 return; 0215 } 0216 auto connection = dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().get(room->localUser()->id())); 0217 Controller::instance().setActiveConnection(connection); 0218 RoomManager::instance().setConnection(connection); 0219 RoomManager::instance().resolveResource(room->id()); 0220 }); 0221 0222 if (canReply) { 0223 std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply"))); 0224 replyAction->setPlaceholderText(i18n("Reply...")); 0225 connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) { 0226 TextHandler textHandler; 0227 textHandler.setData(text); 0228 room->postMessage(text, textHandler.handleSendText(), RoomMessageEvent::MsgType::Text, replyEventId, QString()); 0229 }); 0230 notification->setReplyAction(std::move(replyAction)); 0231 } 0232 0233 notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id()); 0234 notification->sendEvent(); 0235 } 0236 0237 void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom, const QString &title, const QString &sender, const QImage &icon) 0238 { 0239 QPointer room(rawRoom); 0240 QPixmap img; 0241 img.convertFromImage(icon); 0242 KNotification *notification = new KNotification(QStringLiteral("invite")); 0243 notification->setText(i18n("%1 invited you to a room", sender)); 0244 notification->setTitle(title); 0245 notification->setPixmap(createNotificationImage(icon, nullptr)); 0246 notification->setFlags(KNotification::Persistent); 0247 auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat")); 0248 connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() { 0249 if (!room) { 0250 return; 0251 } 0252 WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken()); 0253 notification->close(); 0254 RoomManager::instance().resolveResource(room->id()); 0255 }); 0256 0257 const auto acceptAction = notification->addAction(i18nc("@action:button The thing being accepted is an invitation to chat", "Accept")); 0258 const auto rejectAction = notification->addAction(i18nc("@action:button The thing being rejected is an invitation to chat", "Reject")); 0259 const auto rejectAndIgnoreAction = notification->addAction(i18nc("@action:button The thing being rejected is an invitation to chat", "Reject and Ignore User")); 0260 connect(acceptAction, &KNotificationAction::activated, this, [room, notification]() { 0261 if (!room) { 0262 return; 0263 } 0264 room->acceptInvitation(); 0265 notification->close(); 0266 }); 0267 connect(rejectAction, &KNotificationAction::activated, this, [room, notification]() { 0268 if (!room) { 0269 return; 0270 } 0271 RoomManager::instance().leaveRoom(room); 0272 notification->close(); 0273 }); 0274 connect(rejectAndIgnoreAction, &KNotificationAction::activated, this, [room, notification]() { 0275 if (!room) { 0276 return; 0277 } 0278 RoomManager::instance().leaveRoom(room); 0279 room->connection()->addToIgnoredUsers(room->invitingUser()); 0280 notification->close(); 0281 }); 0282 connect(notification, &KNotification::closed, this, [this, room]() { 0283 if (!room) { 0284 return; 0285 } 0286 m_invitations.remove(room->id()); 0287 }); 0288 0289 notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id()); 0290 0291 notification->sendEvent(); 0292 m_invitations.insert(room->id(), notification); 0293 } 0294 0295 void NotificationsManager::clearInvitationNotification(const QString &roomId) 0296 { 0297 if (m_invitations.contains(roomId)) { 0298 m_invitations[roomId]->close(); 0299 } 0300 } 0301 0302 void NotificationsManager::postPushNotification(const QByteArray &message) 0303 { 0304 const auto json = QJsonDocument::fromJson(message).object(); 0305 0306 const auto type = json["notification"_ls]["type"_ls].toString(); 0307 0308 // the only two types of push notifications we support right now 0309 if (type == QStringLiteral("m.room.message") || type == QStringLiteral("m.room.encrypted")) { 0310 auto notification = new KNotification("message"_ls); 0311 0312 const auto sender = json["notification"_ls]["sender_display_name"_ls].toString(); 0313 const auto roomName = json["notification"_ls]["room_name"_ls].toString(); 0314 const auto roomId = json["notification"_ls]["room_id"_ls].toString(); 0315 0316 if (roomName.isEmpty() || sender == roomName) { 0317 notification->setTitle(sender); 0318 } else { 0319 notification->setTitle(i18n("%1 (%2)", sender, roomName)); 0320 } 0321 0322 if (type == QStringLiteral("m.room.message")) { 0323 const auto text = json["notification"_ls]["content"_ls]["body"_ls].toString(); 0324 notification->setText(text.toHtmlEscaped()); 0325 } else if (type == QStringLiteral("m.room.encrypted")) { 0326 notification->setText(i18n("Encrypted Message")); 0327 } 0328 0329 #ifdef HAVE_KIO 0330 auto openAction = notification->addAction(i18n("Open NeoChat")); 0331 connect(openAction, &KNotificationAction::activated, this, [=]() { 0332 QString properId = roomId; 0333 properId = properId.replace(QStringLiteral("#"), QString()); 0334 properId = properId.replace(QStringLiteral("!"), QString()); 0335 0336 auto *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.neochat"))); 0337 job->setUrls({QUrl::fromUserInput(QStringLiteral("matrix:r/%1").arg(properId))}); 0338 job->start(); 0339 }); 0340 #endif 0341 0342 connect(notification, &KNotification::closed, qGuiApp, &QGuiApplication::quit); 0343 0344 notification->sendEvent(); 0345 0346 m_notifications.insert(roomId, notification); 0347 } else { 0348 qWarning() << "Skipping unsupported push notification" << type; 0349 } 0350 } 0351 0352 QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room) 0353 { 0354 // Handle avatars that are lopsided in one dimension 0355 const int biggestDimension = std::max(icon.width(), icon.height()); 0356 const QRect imageRect{0, 0, biggestDimension, biggestDimension}; 0357 0358 QImage roundedImage(imageRect.size(), QImage::Format_ARGB32); 0359 roundedImage.fill(Qt::transparent); 0360 0361 QPainter painter(&roundedImage); 0362 painter.setRenderHint(QPainter::SmoothPixmapTransform); 0363 painter.setPen(Qt::NoPen); 0364 0365 // Fill background for transparent avatars 0366 painter.setBrush(Qt::white); 0367 painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height()); 0368 0369 QBrush brush(icon.scaledToHeight(biggestDimension)); 0370 painter.setBrush(brush); 0371 painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height()); 0372 0373 if (room != nullptr) { 0374 const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height()); 0375 if (icon != roomAvatar) { 0376 const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2}; 0377 0378 painter.setBrush(Qt::white); 0379 painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height()); 0380 0381 painter.setBrush(roomAvatar.scaled(lowerQuarter.size())); 0382 painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height()); 0383 } 0384 } 0385 0386 return QPixmap::fromImage(roundedImage); 0387 } 0388 0389 #include "moc_notificationsmanager.cpp"