446 lines
17 KiB
C++
446 lines
17 KiB
C++
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "notificationsmanager.h"
|
|
|
|
#include <memory>
|
|
|
|
#include <QGuiApplication>
|
|
|
|
#include <KLocalizedString>
|
|
#include <KNotification>
|
|
#include <KNotificationPermission>
|
|
#include <KNotificationReplyAction>
|
|
|
|
#include <QPainter>
|
|
#include <Quotient/accountregistry.h>
|
|
#include <Quotient/csapi/pushrules.h>
|
|
#include <Quotient/events/roommemberevent.h>
|
|
#include <Quotient/user.h>
|
|
|
|
#ifdef HAVE_KIO
|
|
#include <KIO/ApplicationLauncherJob>
|
|
#endif
|
|
|
|
#include "controller.h"
|
|
#include "jobs/neochatgetcommonroomsjob.h"
|
|
#include "neochatconfig.h"
|
|
#include "neochatconnection.h"
|
|
#include "neochatroom.h"
|
|
#include "roommanager.h"
|
|
#include "texthandler.h"
|
|
#include "windowcontroller.h"
|
|
|
|
using namespace Quotient;
|
|
|
|
NotificationsManager::NotificationsManager(QObject *parent)
|
|
: QObject(parent)
|
|
{
|
|
}
|
|
|
|
void NotificationsManager::handleNotifications(const QPointer<NeoChatConnection> &connection)
|
|
{
|
|
if (KNotificationPermission::checkPermission() == Qt::PermissionStatus::Granted) {
|
|
startNotificationJob(connection);
|
|
} else if (!permissionAsked) {
|
|
KNotificationPermission::requestPermission(this, [this, connection](Qt::PermissionStatus result) {
|
|
if (result == Qt::PermissionStatus::Granted) {
|
|
startNotificationJob(connection);
|
|
} else {
|
|
permissionAsked = true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void NotificationsManager::startNotificationJob(QPointer<NeoChatConnection> connection)
|
|
{
|
|
if (connection == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (!m_connActiveJob.contains(connection->user()->id())) {
|
|
m_connActiveJob.append(connection->user()->id());
|
|
connection->callApi<GetNotificationsJob>().onResult([this, connection](const auto &job) {
|
|
m_connActiveJob.removeAll(connection->user()->id());
|
|
processNotificationJob(connection, job, !m_oldNotifications.contains(connection->user()->id()));
|
|
});
|
|
}
|
|
}
|
|
|
|
void NotificationsManager::processNotificationJob(const QPointer<NeoChatConnection> &connection, const GetNotificationsJob *job, const bool initialization)
|
|
{
|
|
if (!job || !connection || !connection->isLoggedIn()) {
|
|
return;
|
|
}
|
|
|
|
const auto connectionId = connection->user()->id();
|
|
|
|
const auto notifications = job->jsonData()["notifications"_L1].toArray();
|
|
if (initialization) {
|
|
for (const auto ¬ification : notifications) {
|
|
if (!m_initialTimestamp.contains(connectionId)) {
|
|
m_initialTimestamp[connectionId] = notification["ts"_L1].toVariant().toLongLong();
|
|
} else {
|
|
if (const auto timestamp = notification["ts"_L1].toVariant().toLongLong(); timestamp > m_initialTimestamp[connectionId]) {
|
|
m_initialTimestamp[connectionId] = timestamp;
|
|
}
|
|
}
|
|
|
|
auto connectionNotifications = m_oldNotifications.value(connectionId);
|
|
connectionNotifications += notification["event"_L1]["event_id"_L1].toString();
|
|
m_oldNotifications[connectionId] = connectionNotifications;
|
|
}
|
|
return;
|
|
}
|
|
|
|
QMap<QString, std::pair<qint64, QJsonObject>> notificationsToPost;
|
|
for (const auto &n : notifications) {
|
|
const auto notification = n.toObject();
|
|
if (notification["read"_L1].toBool()) {
|
|
continue;
|
|
}
|
|
auto connectionNotifications = m_oldNotifications.value(connectionId);
|
|
if (connectionNotifications.contains(notification["event"_L1]["event_id"_L1].toString())) {
|
|
continue;
|
|
}
|
|
connectionNotifications += notification["event"_L1]["event_id"_L1].toString();
|
|
m_oldNotifications[connectionId] = connectionNotifications;
|
|
|
|
if (!shouldPostNotification(connection, n)) {
|
|
continue;
|
|
}
|
|
|
|
const auto &roomId = notification["room_id"_L1].toString();
|
|
if (!notificationsToPost.contains(roomId) || notificationsToPost[roomId].first < notification["ts"_L1].toVariant().toLongLong()) {
|
|
notificationsToPost[roomId] = {notification["ts"_L1].toVariant().toLongLong(), notification};
|
|
}
|
|
}
|
|
|
|
for (const auto &[roomId, pair] : notificationsToPost.asKeyValueRange()) {
|
|
const auto ¬ification = pair.second;
|
|
const auto room = connection->room(roomId);
|
|
if (!room) {
|
|
continue;
|
|
}
|
|
auto sender = room->member(notification["event"_L1]["sender"_L1].toString());
|
|
|
|
if (room->joinState() == JoinState::Invite) {
|
|
postInviteNotification(qobject_cast<NeoChatRoom *>(room));
|
|
continue;
|
|
}
|
|
|
|
QString body;
|
|
if (notification["event"_L1]["type"_L1].toString() == "org.matrix.msc3381.poll.start"_L1) {
|
|
body = notification["event"_L1]["content"_L1]["org.matrix.msc3381.poll.start"_L1]["question"_L1]["body"_L1].toString();
|
|
} else if (notification["event"_L1]["type"_L1] == "m.room.encrypted"_L1) {
|
|
const auto decrypted = connection->decryptNotification(notification);
|
|
body = decrypted["content"_L1]["body"_L1].toString();
|
|
if (body.isEmpty()) {
|
|
body = i18n("Encrypted Message");
|
|
}
|
|
} else {
|
|
body = notification["event"_L1]["content"_L1]["body"_L1].toString();
|
|
}
|
|
|
|
QImage avatar_image;
|
|
if (!sender.avatarUrl().isEmpty()) {
|
|
avatar_image = room->member(sender.id()).avatar(128, 128, {});
|
|
} else {
|
|
avatar_image = room->avatar(128);
|
|
}
|
|
postNotification(dynamic_cast<NeoChatRoom *>(room),
|
|
sender.displayName(),
|
|
body,
|
|
avatar_image,
|
|
notification["event"_L1].toObject()["event_id"_L1].toString(),
|
|
true,
|
|
pair.first);
|
|
}
|
|
}
|
|
|
|
bool NotificationsManager::shouldPostNotification(const QPointer<NeoChatConnection> &connection, const QJsonValue ¬ification)
|
|
{
|
|
if (connection == nullptr || !connection->isLoggedIn()) {
|
|
return false;
|
|
}
|
|
|
|
const auto room = connection->room(notification["room_id"_L1].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 set up so that if the application is inactive, the notification will
|
|
// always be posted, even if the room is the current room.
|
|
if (RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id()
|
|
&& 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.
|
|
const auto timestamp = notification["ts"_L1].toDouble();
|
|
if (timestamp < m_initialTimestamp[connection->user()->id()]) {
|
|
return false;
|
|
}
|
|
|
|
if (m_notifications.contains(room->id()) && m_notifications[room->id()].first > timestamp) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NotificationsManager::postNotification(NeoChatRoom *room,
|
|
const QString &sender,
|
|
const QString &text,
|
|
const QImage &icon,
|
|
const QString &replyEventId,
|
|
const bool canReply,
|
|
qint64 timestamp)
|
|
{
|
|
const QString roomId = room->id();
|
|
|
|
if (auto notification = m_notifications.value(roomId).second) {
|
|
notification->close();
|
|
}
|
|
|
|
auto notification = new KNotification(u"message"_s);
|
|
m_notifications.insert(roomId, {timestamp, notification});
|
|
connect(notification, &KNotification::closed, this, [this, roomId, notification] {
|
|
if (m_notifications[roomId].second == notification) {
|
|
m_notifications.remove(roomId);
|
|
}
|
|
});
|
|
|
|
notification->setTitle(room->displayName());
|
|
|
|
QString entry;
|
|
if (room->isDirectChat()) {
|
|
entry = text.toHtmlEscaped();
|
|
} else {
|
|
entry = i18n("%1: %2", sender, text.toHtmlEscaped());
|
|
}
|
|
|
|
notification->setText(entry);
|
|
notification->setPixmap(createNotificationImage(icon, room));
|
|
|
|
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
|
|
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
|
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
|
|
if (!room) {
|
|
return;
|
|
}
|
|
auto connection = dynamic_cast<NeoChatConnection *>(Controller::instance().accounts()->get(room->localMember().id()));
|
|
Controller::instance().setActiveConnection(connection);
|
|
RoomManager::instance().setConnection(connection);
|
|
RoomManager::instance().resolveResource(room->id());
|
|
});
|
|
|
|
if (canReply) {
|
|
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
|
replyAction->setPlaceholderText(i18n("Reply…"));
|
|
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
|
|
TextHandler textHandler;
|
|
textHandler.setData(text);
|
|
auto content = std::make_unique<Quotient::EventContent::TextContent>(textHandler.handleSendText(), u"text/html"_s);
|
|
EventRelation relatesTo = EventRelation::replyTo(replyEventId);
|
|
room->post<Quotient::RoomMessageEvent>(text, MessageEventType::Text, std::move(content), relatesTo);
|
|
});
|
|
notification->setReplyAction(std::move(replyAction));
|
|
}
|
|
|
|
if (Controller::instance().accounts()->rowCount() > 1) {
|
|
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
|
|
}
|
|
notification->sendEvent();
|
|
}
|
|
|
|
void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom)
|
|
{
|
|
QPointer room(rawRoom);
|
|
|
|
const auto roomMemberEvent = room->currentState().get<RoomMemberEvent>(room->localMember().id());
|
|
if (roomMemberEvent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (NeoChatConfig::rejectUnknownInvites()) {
|
|
auto job = room->connection()->callApi<NeochatGetCommonRoomsJob>(roomMemberEvent->senderId());
|
|
connect(job, &BaseJob::result, this, [this, job, room] {
|
|
if (QJsonObject replyData = job->jsonData(); replyData.contains(u"joined"_s)) {
|
|
if (!replyData["joined"_L1].toArray().isEmpty()) {
|
|
doPostInviteNotification(room);
|
|
} else {
|
|
room->forget();
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
doPostInviteNotification(room);
|
|
}
|
|
}
|
|
|
|
void NotificationsManager::doPostInviteNotification(const QPointer<NeoChatRoom> &room)
|
|
{
|
|
const auto roomMemberEvent = room->currentState().get<RoomMemberEvent>(room->localMember().id());
|
|
if (roomMemberEvent == nullptr) {
|
|
return;
|
|
}
|
|
const auto sender = room->member(roomMemberEvent->senderId());
|
|
|
|
QImage avatar_image;
|
|
if (!room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
|
|
avatar_image = room->member(roomMemberEvent->senderId()).avatar(128, 128, {});
|
|
} else {
|
|
qWarning() << "using this room's avatar";
|
|
avatar_image = room->avatar(128);
|
|
}
|
|
|
|
const auto notification = new KNotification(u"invite"_s);
|
|
notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName()));
|
|
notification->setTitle(room->displayName());
|
|
notification->setPixmap(createNotificationImage(avatar_image, nullptr));
|
|
const auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
|
|
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
|
if (!room) {
|
|
return;
|
|
}
|
|
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
|
|
notification->close();
|
|
RoomManager::instance().resolveResource(room->id());
|
|
});
|
|
|
|
const auto acceptAction = notification->addAction(i18nc("@action:button The thing being accepted is an invitation to chat", "Accept"));
|
|
const auto rejectAction = notification->addAction(i18nc("@action:button The thing being rejected is an invitation to chat", "Reject"));
|
|
const auto rejectAndIgnoreAction =
|
|
notification->addAction(i18nc("@action:button The thing being rejected is an invitation to chat", "Reject and Ignore User"));
|
|
connect(acceptAction, &KNotificationAction::activated, this, [room, notification]() {
|
|
if (!room) {
|
|
return;
|
|
}
|
|
room->acceptInvitation();
|
|
notification->close();
|
|
});
|
|
connect(rejectAction, &KNotificationAction::activated, this, [room, notification]() {
|
|
if (!room) {
|
|
return;
|
|
}
|
|
room->forget();
|
|
notification->close();
|
|
});
|
|
connect(rejectAndIgnoreAction, &KNotificationAction::activated, this, [room, notification]() {
|
|
if (!room) {
|
|
return;
|
|
}
|
|
room->forget();
|
|
room->connection()->addToIgnoredUsers(room->invitingUserId());
|
|
notification->close();
|
|
});
|
|
connect(notification, &KNotification::closed, this, [this, room]() {
|
|
if (!room) {
|
|
return;
|
|
}
|
|
m_invitations.remove(room->id());
|
|
});
|
|
|
|
if (Controller::instance().accounts()->rowCount() > 1) {
|
|
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
|
|
}
|
|
|
|
notification->sendEvent();
|
|
}
|
|
|
|
void NotificationsManager::clearInvitationNotification(const QString &roomId)
|
|
{
|
|
if (m_invitations.contains(roomId)) {
|
|
m_invitations[roomId]->close();
|
|
}
|
|
}
|
|
|
|
void NotificationsManager::postPushNotification(const QByteArray &message)
|
|
{
|
|
const auto json = QJsonDocument::fromJson(message).object();
|
|
|
|
// the only two types of push notifications we support right now
|
|
if (const auto type = json["notification"_L1]["type"_L1].toString(); type == u"m.room.message"_s || type == u"m.room.encrypted"_s) {
|
|
const auto notification = new KNotification("message"_L1);
|
|
|
|
const auto sender = json["notification"_L1]["sender_display_name"_L1].toString();
|
|
const auto roomName = json["notification"_L1]["room_name"_L1].toString();
|
|
const auto roomId = json["notification"_L1]["room_id"_L1].toString();
|
|
|
|
if (roomName.isEmpty() || sender == roomName) {
|
|
notification->setTitle(sender);
|
|
} else {
|
|
notification->setTitle(i18n("%1 (%2)", sender, roomName));
|
|
}
|
|
|
|
if (type == u"m.room.message"_s) {
|
|
const auto text = json["notification"_L1]["content"_L1]["body"_L1].toString();
|
|
notification->setText(text.toHtmlEscaped());
|
|
} else if (type == u"m.room.encrypted"_s) {
|
|
notification->setText(i18n("Encrypted Message"));
|
|
}
|
|
|
|
#ifdef HAVE_KIO
|
|
const auto openAction = notification->addAction(i18n("Open NeoChat"));
|
|
connect(openAction, &KNotificationAction::activated, notification, [=]() {
|
|
QString properId = roomId;
|
|
properId = properId.replace(u"#"_s, QString());
|
|
properId = properId.replace(u"!"_s, QString());
|
|
|
|
const auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(u"org.kde.neochat"_s));
|
|
job->setUrls({QUrl::fromUserInput(u"matrix:r/%1"_s.arg(properId))});
|
|
job->start();
|
|
});
|
|
#endif
|
|
|
|
connect(notification, &KNotification::closed, qGuiApp, &QGuiApplication::quit);
|
|
|
|
notification->sendEvent();
|
|
} else {
|
|
qWarning() << "Skipping unsupported push notification" << type;
|
|
}
|
|
}
|
|
|
|
QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
|
|
{
|
|
// Handle avatars that are lopsided in one dimension
|
|
const int biggestDimension = std::max(icon.width(), icon.height());
|
|
const QRect imageRect{0, 0, biggestDimension, biggestDimension};
|
|
|
|
QImage roundedImage(imageRect.size(), QImage::Format_ARGB32);
|
|
roundedImage.fill(Qt::transparent);
|
|
|
|
QPainter painter(&roundedImage);
|
|
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
|
painter.setPen(Qt::NoPen);
|
|
|
|
// Fill background for transparent avatars
|
|
painter.setBrush(Qt::white);
|
|
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
|
|
|
const QBrush brush(icon.scaledToHeight(biggestDimension));
|
|
painter.setBrush(brush);
|
|
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
|
|
|
if (room) {
|
|
if (const auto roomAvatar = room->avatar(imageRect.width(), imageRect.height()); !roomAvatar.isNull() && icon != roomAvatar) {
|
|
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
|
|
|
|
painter.setBrush(Qt::white);
|
|
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
|
|
|
|
painter.setBrush(roomAvatar.scaled(lowerQuarter.size()));
|
|
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
|
|
}
|
|
}
|
|
|
|
return QPixmap::fromImage(roundedImage);
|
|
}
|
|
|
|
#include "moc_notificationsmanager.cpp"
|