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 ¬ification) 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"