summary refs log tree commit diff
path: root/src/notifications
diff options
context:
space:
mode:
authorDeepBlueV7.X <nicolas.werner@hotmail.de>2021-03-18 15:46:04 +0100
committerGitHub <noreply@github.com>2021-03-18 15:46:04 +0100
commitf6de66576cc80e7767f2084d4b3efaf08aaad56d (patch)
tree6e26705fda350a3c2a4e014f4b5ac61f40fb2dad /src/notifications
parentMerge pull request #527 from anjanik012/ignore_event (diff)
parentMake CI happy (diff)
downloadnheko-f6de66576cc80e7767f2084d4b3efaf08aaad56d.tar.xz
Merge pull request #475 from LorenDB/htmlFormattedNotifs
Better notifications
Diffstat (limited to 'src/notifications')
-rw-r--r--src/notifications/Manager.cpp40
-rw-r--r--src/notifications/Manager.h43
-rw-r--r--src/notifications/ManagerLinux.cpp139
-rw-r--r--src/notifications/ManagerMac.cpp66
-rw-r--r--src/notifications/ManagerMac.mm37
-rw-r--r--src/notifications/ManagerWin.cpp67
6 files changed, 320 insertions, 72 deletions
diff --git a/src/notifications/Manager.cpp b/src/notifications/Manager.cpp
new file mode 100644

index 00000000..be580b08 --- /dev/null +++ b/src/notifications/Manager.cpp
@@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "notifications/Manager.h" + +#include "Cache.h" +#include "EventAccessors.h" +#include "Utils.h" + +QString +NotificationsManager::getMessageTemplate(const mtx::responses::Notification &notification) +{ + const auto sender = + cache::displayName(QString::fromStdString(notification.room_id), + QString::fromStdString(mtx::accessors::sender(notification.event))); + + // TODO: decrypt this message if the decryption setting is on in the UserSettings + if (auto msg = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + &notification.event); + msg != nullptr) { + return tr("%1 sent an encrypted message").arg(sender); + } + + if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) { + return tr("* %1 %2", + "Format an emote message in a notification, %1 is the sender, %2 the " + "message") + .arg(sender); + } else if (utils::isReply(notification.event)) { + return tr("%1 replied: %2", + "Format a reply in a notification. %1 is the sender, %2 the message") + .arg(sender); + } else { + return tr("%1: %2", + "Format a normal message in a notification. %1 is the sender, %2 the " + "message") + .arg(sender); + } +} diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h
index e2b3236a..416530e0 100644 --- a/src/notifications/Manager.h +++ b/src/notifications/Manager.h
@@ -10,7 +10,12 @@ #include <mtx/responses/notifications.hpp> +// convenience definition #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) +#define NHEKO_DBUS_SYS +#endif + +#if defined(NHEKO_DBUS_SYS) #include <QtDBus/QDBusArgument> #include <QtDBus/QDBusInterface> #endif @@ -38,20 +43,51 @@ public: signals: void notificationClicked(const QString roomId, const QString eventId); void sendNotificationReply(const QString roomId, const QString eventId, const QString body); + void systemPostNotificationCb(const QString &room_id, + const QString &event_id, + const QString &roomName, + const QString &text, + const QImage &icon); public slots: void removeNotification(const QString &roomId, const QString &eventId); -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) +#if defined(NHEKO_DBUS_SYS) public: void closeNotifications(QString roomId); private: QDBusInterface dbus; + + void systemPostNotification(const QString &room_id, + const QString &event_id, + const QString &roomName, + const QString &text, + const QImage &icon); void closeNotification(uint id); // notification ID to (room ID, event ID) QMap<uint, roomEventId> notificationIds; + + const bool hasMarkup_; + const bool hasImages_; +#endif + +#if defined(Q_OS_MACOS) +private: + // Objective-C(++) doesn't like to do lots of regular C++, so the actual notification + // posting is split out + void objCxxPostNotification(const QString &title, + const QString &subtitle, + const QString &informativeText, + const QImage &bodyImage); +#endif + +#if defined(Q_OS_WINDOWS) +private: + void systemPostNotification(const QString &line1, + const QString &line2, + const QString &iconPath); #endif // these slots are platform specific (D-Bus only) @@ -60,9 +96,12 @@ private slots: void actionInvoked(uint id, QString action); void notificationClosed(uint id, uint reason); void notificationReplied(uint id, QString reply); + +private: + QString getMessageTemplate(const mtx::responses::Notification &notification); }; -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) +#if defined(NHEKO_DBUS_SYS) QDBusArgument & operator<<(QDBusArgument &arg, const QImage &image); const QDBusArgument & diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index a222bd36..598b2bd0 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp
@@ -1,3 +1,8 @@ +// SPDX-FileCopyrightText: 2012 Roland Hieber <rohieb@rohieb.name> +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + #include "notifications/Manager.h" #include <QDBusConnection> @@ -7,12 +12,19 @@ #include <QDBusPendingReply> #include <QDebug> #include <QImage> +#include <QRegularExpression> +#include <QStringBuilder> +#include <QTextDocumentFragment> + +#include <functional> +#include <variant> + +#include <mtx/responses/notifications.hpp> #include "Cache.h" #include "EventAccessors.h" -#include "MatrixClient.h" +#include "MxcImageProvider.h" #include "Utils.h" -#include <mtx/responses/notifications.hpp> NotificationsManager::NotificationsManager(QObject *parent) : QObject(parent) @@ -21,6 +33,18 @@ NotificationsManager::NotificationsManager(QObject *parent) "org.freedesktop.Notifications", QDBusConnection::sessionBus(), this) + , hasMarkup_{std::invoke([this]() -> bool { + for (auto x : dbus.call("GetCapabilities").arguments()) + if (x.toStringList().contains("body-markup")) + return true; + return false; + })} + , hasImages_{std::invoke([this]() -> bool { + for (auto x : dbus.call("GetCapabilities").arguments()) + if (x.toStringList().contains("body-images")) + return true; + return false; + })} { qDBusRegisterMetaType<QImage>(); @@ -42,12 +66,13 @@ NotificationsManager::NotificationsManager(QObject *parent) "NotificationReplied", this, SLOT(notificationReplied(uint, QString))); -} -// SPDX-FileCopyrightText: 2012 Roland Hieber <rohieb@rohieb.name> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later + connect(this, + &NotificationsManager::systemPostNotificationCb, + this, + &NotificationsManager::systemPostNotification, + Qt::QueuedConnection); +} void NotificationsManager::postNotification(const mtx::responses::Notification &notification, @@ -55,25 +80,87 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif { const auto room_id = QString::fromStdString(notification.room_id); const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event)); - const auto sender = cache::displayName( - room_id, QString::fromStdString(mtx::accessors::sender(notification.event))); - const auto text = utils::event_body(notification.event); + const auto room_name = + QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); + auto postNotif = [this, room_id, event_id, room_name, icon](QString text) { + emit systemPostNotificationCb(room_id, event_id, room_name, text, icon); + }; + + QString template_ = getMessageTemplate(notification); + // TODO: decrypt this message if the decryption setting is on in the UserSettings + if (std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + notification.event)) { + postNotif(template_); + return; + } + + if (hasMarkup_) { + if (hasImages_ && mtx::accessors::msg_type(notification.event) == + mtx::events::MessageType::Image) { + MxcImageProvider::download( + QString::fromStdString(mtx::accessors::url(notification.event)) + .remove("mxc://"), + QSize(200, 80), + [postNotif, notification, template_]( + QString, QSize, QImage, QString imgPath) { + if (imgPath.isEmpty()) + postNotif(template_ + .arg(utils::stripReplyFallbacks( + notification.event, {}, {}) + .quoted_formatted_body) + .replace("<em>", "<i>") + .replace("</em>", "</i>") + .replace("<strong>", "<b>") + .replace("</strong>", "</b>")); + else + postNotif(template_.arg( + QStringLiteral("<br><img src=\"file:///") % imgPath % + "\" alt=\"" % + mtx::accessors::formattedBodyWithFallback( + notification.event) % + "\">")); + }); + return; + } + + postNotif( + template_ + .arg( + utils::stripReplyFallbacks(notification.event, {}, {}).quoted_formatted_body) + .replace("<em>", "<i>") + .replace("</em>", "</i>") + .replace("<strong>", "<b>") + .replace("</strong>", "</b>")); + return; + } + + postNotif( + template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body)); +} + +/** + * This function is based on code from + * https://github.com/rohieb/StratumsphereTrayIcon + * Copyright (C) 2012 Roland Hieber <rohieb@rohieb.name> + * Licensed under the GNU General Public License, version 3 + */ +void +NotificationsManager::systemPostNotification(const QString &room_id, + const QString &event_id, + const QString &roomName, + const QString &text, + const QImage &icon) +{ QVariantMap hints; hints["image-data"] = icon; hints["sound-name"] = "message-new-instant"; QList<QVariant> argumentList; - argumentList << "nheko"; // app_name - argumentList << (uint)0; // replace_id - argumentList << ""; // app_icon - argumentList << QString::fromStdString( - cache::singleRoomInfo(notification.room_id).name); // summary - - // body - if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) - argumentList << "* " + sender + " " + text; - else - argumentList << sender + ": " + text; + argumentList << "nheko"; // app_name + argumentList << (uint)0; // replace_id + argumentList << ""; // app_icon + argumentList << roomName; // summary + argumentList << text; // body // The list of actions has always the action name and then a localized version of that // action. Currently we just use an empty string for that. @@ -84,10 +171,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif argumentList << hints; // hints argumentList << (int)-1; // timeout in ms - static QDBusInterface notifyApp("org.freedesktop.Notifications", - "/org/freedesktop/Notifications", - "org.freedesktop.Notifications"); - QDBusPendingCall call = notifyApp.asyncCallWithArgumentList("Notify", argumentList); + QDBusPendingCall call = dbus.asyncCallWithArgumentList("Notify", argumentList); auto watcher = new QDBusPendingCallWatcher{call, this}; connect( watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() { @@ -103,10 +187,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif void NotificationsManager::closeNotification(uint id) { - static QDBusInterface closeCall("org.freedesktop.Notifications", - "/org/freedesktop/Notifications", - "org.freedesktop.Notifications"); - auto call = closeCall.asyncCall("CloseNotification", (uint)id); // replace_id + auto call = dbus.asyncCall("CloseNotification", (uint)id); // replace_id auto watcher = new QDBusPendingCallWatcher{call, this}; connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() { if (watcher->reply().type() == QDBusMessage::ErrorMessage) { diff --git a/src/notifications/ManagerMac.cpp b/src/notifications/ManagerMac.cpp new file mode 100644
index 00000000..8e36985c --- /dev/null +++ b/src/notifications/ManagerMac.cpp
@@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Manager.h" + +#include <QRegularExpression> +#include <QTextDocumentFragment> + +#include "Cache.h" +#include "EventAccessors.h" +#include "MxcImageProvider.h" +#include "Utils.h" + +#include <mtx/responses/notifications.hpp> + +#include <variant> + +static QString +formatNotification(const mtx::responses::Notification &notification) +{ + return utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body; +} + +void +NotificationsManager::postNotification(const mtx::responses::Notification &notification, + const QImage &icon) +{ + Q_UNUSED(icon) + + const auto room_name = + QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); + const auto sender = + cache::displayName(QString::fromStdString(notification.room_id), + QString::fromStdString(mtx::accessors::sender(notification.event))); + + const auto isEncrypted = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + &notification.event) != nullptr; + const auto isReply = utils::isReply(notification.event); + if (isEncrypted) { + // TODO: decrypt this message if the decryption setting is on in the UserSettings + const QString messageInfo = (isReply ? tr("%1 replied with an encrypted message") + : tr("%1 sent an encrypted message")) + .arg(sender); + objCxxPostNotification(room_name, messageInfo, "", QImage()); + } else { + const QString messageInfo = + (isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender); + if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image) + MxcImageProvider::download( + QString::fromStdString(mtx::accessors::url(notification.event)) + .remove("mxc://"), + QSize(200, 80), + [this, notification, room_name, messageInfo]( + QString, QSize, QImage image, QString) { + objCxxPostNotification(room_name, + messageInfo, + formatNotification(notification), + image); + }); + else + objCxxPostNotification( + room_name, messageInfo, formatNotification(notification), QImage()); + } +} diff --git a/src/notifications/ManagerMac.mm b/src/notifications/ManagerMac.mm
index 5609d3de..33b7b6af 100644 --- a/src/notifications/ManagerMac.mm +++ b/src/notifications/ManagerMac.mm
@@ -1,13 +1,10 @@ #include "notifications/Manager.h" -#include <Foundation/Foundation.h> -#include <QtMac> +#import <Foundation/Foundation.h> +#import <AppKit/NSImage.h> -#include "Cache.h" -#include "EventAccessors.h" -#include "MatrixClient.h" -#include "Utils.h" -#include <mtx/responses/notifications.hpp> +#include <QtMac> +#include <QImage> @interface NSUserNotification (CFIPrivate) - (void)set_identityImage:(NSImage *)image; @@ -19,24 +16,22 @@ NotificationsManager::NotificationsManager(QObject *parent): QObject(parent) } void -NotificationsManager::postNotification(const mtx::responses::Notification &notification, - const QImage &icon) +NotificationsManager::objCxxPostNotification(const QString &title, + const QString &subtitle, + const QString &informativeText, + const QImage &bodyImage) { - Q_UNUSED(icon); - - const auto sender = cache::displayName(QString::fromStdString(notification.room_id), QString::fromStdString(mtx::accessors::sender(notification.event))); - const auto text = utils::event_body(notification.event); - NSUserNotification * notif = [[NSUserNotification alloc] init]; + NSUserNotification *notif = [[NSUserNotification alloc] init]; - notif.title = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name).toNSString(); - notif.subtitle = QString("%1 sent a message").arg(sender).toNSString(); - if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) - notif.informativeText = QString("* ").append(sender).append(" ").append(text).toNSString(); - else - notif.informativeText = text.toNSString(); + notif.title = title.toNSString(); + notif.subtitle = subtitle.toNSString(); + notif.informativeText = informativeText.toNSString(); notif.soundName = NSUserNotificationDefaultSoundName; + if (!bodyImage.isNull()) + notif.contentImage = [[NSImage alloc] initWithCGImage: bodyImage.toCGImage() size: NSZeroSize]; + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif]; [notif autorelease]; } @@ -45,7 +40,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif void NotificationsManager::actionInvoked(uint, QString) { - } +} void NotificationsManager::notificationReplied(uint, QString) diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp
index 3152d84f..fe7830a7 100644 --- a/src/notifications/ManagerWin.cpp +++ b/src/notifications/ManagerWin.cpp
@@ -5,11 +5,15 @@ #include "notifications/Manager.h" #include "wintoastlib.h" +#include <QRegularExpression> +#include <QStandardPaths> +#include <QTextDocumentFragment> + +#include <variant> + #include "Cache.h" #include "EventAccessors.h" -#include "MatrixClient.h" #include "Utils.h" -#include <mtx/responses/notifications.hpp> using namespace WinToastLib; @@ -45,34 +49,57 @@ void NotificationsManager::postNotification(const mtx::responses::Notification &notification, const QImage &icon) { - Q_UNUSED(icon) - const auto room_name = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); const auto sender = cache::displayName(QString::fromStdString(notification.room_id), QString::fromStdString(mtx::accessors::sender(notification.event))); - const auto text = utils::event_body(notification.event); + const auto isEncrypted = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + &notification.event) != nullptr; + const auto isReply = utils::isReply(notification.event); + + auto formatNotification = [this, notification, sender] { + const auto template_ = getMessageTemplate(notification); + if (std::holds_alternative< + mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + notification.event)) { + return template_; + } + + return template_.arg( + utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body); + }; + + const auto line1 = + (room_name == sender) ? sender : QString("%1 - %2").arg(sender).arg(room_name); + const auto line2 = (isEncrypted ? (isReply ? tr("%1 replied with an encrypted message") + : tr("%1 sent an encrypted message")) + : formatNotification()); + + auto iconPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + room_name + "-room-avatar.png"; + if (!icon.save(iconPath)) + iconPath.clear(); + + systemPostNotification(line1, line2, iconPath); +} + +void +NotificationsManager::systemPostNotification(const QString &line1, + const QString &line2, + const QString &iconPath) +{ if (!isInitialized) init(); auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02); - if (room_name != sender) - templ.setTextField(QString("%1 - %2").arg(sender).arg(room_name).toStdWString(), - WinToastTemplate::FirstLine); - else - templ.setTextField(QString("%1").arg(sender).toStdWString(), - WinToastTemplate::FirstLine); - if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) - templ.setTextField( - QString("* ").append(sender).append(" ").append(text).toStdWString(), - WinToastTemplate::SecondLine); - else - templ.setTextField(QString("%1").arg(text).toStdWString(), - WinToastTemplate::SecondLine); - // TODO: implement room or user avatar - // templ.setImagePath(L"C:/example.png"); + templ.setTextField(line1.toStdWString(), WinToastTemplate::FirstLine); + templ.setTextField(line2.toStdWString(), WinToastTemplate::SecondLine); + + if (!iconPath.isNull()) + templ.setImagePath(iconPath.toStdWString()); WinToast::instance()->showToast(templ, new CustomHandler()); }