diff --git a/src/notifications/Manager.cpp b/src/notifications/Manager.cpp index 1e4d8167..6fa06cb2 100644 --- a/src/notifications/Manager.cpp +++ b/src/notifications/Manager.cpp @@ -2,32 +2,76 @@ #include "Cache.h" #include "EventAccessors.h" +#include "Logging.h" +#include "MatrixClient.h" #include "Utils.h" -#include -void -NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, - const QImage &icon) +#include +#include +#include + +#include + +QString +NotificationsManager::cacheImage(const mtx::events::collections::TimelineEvents &event) { - const auto room_id = QString::fromStdString(notification.room_id); - const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event)); - const auto room_name = - QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); - const auto sender = cache::displayName( - room_id, QString::fromStdString(mtx::accessors::sender(notification.event))); + const auto url = mtx::accessors::url(event); + auto encryptionInfo = mtx::accessors::file(event); - const QString reply = (utils::isReply(notification.event) - ? tr(" replied", - "Used to denote that this message is a reply to another " - "message. Displayed as 'foo replied: message'.") - : ""); + auto filename = QString::fromStdString(mtx::accessors::body(event)); + QString path{QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + + filename}; - // the "replied" is only added if this message is not an emote message - QString text = - ((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) - ? "* " + sender + " " - : sender + reply + ": ") + - formatNotification(notification.event); + http::client()->download( + url, + [path, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } - systemPostNotification(room_id, event_id, room_name, sender, text, icon); + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file{path}; + + if (!file.open(QIODevice::WriteOnly)) + return; + + // delete any existing file content + file.resize(0); + file.write(QByteArray(temp.data(), (int)temp.size())); + + // resize the image (really inefficient, I know, but I can't find any + // better way right off + QImage img{path}; + + // delete existing contents + file.resize(0); + + // make sure to save as PNG (because Plasma doesn't do JPEG in + // notifications) + // if (!file.fileName().endsWith(".png")) + // file.rename(file.fileName() + ".png"); + + img.scaled(200, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation) + .save(&file); + file.close(); + + return; + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while caching file to: {}", e.what()); + } + }); + + return path.toHtmlEscaped(); } diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h index 449a609f..ef049914 100644 --- a/src/notifications/Manager.h +++ b/src/notifications/Manager.h @@ -10,7 +10,12 @@ #include +// 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 #include #endif @@ -43,25 +48,46 @@ public slots: void removeNotification(const QString &roomId, const QString &eventId); private: - void systemPostNotification(const QString &room_id, - const QString &event_id, - const QString &roomName, - const QString &sender, - const QString &text, - const QImage &icon); + QString cacheImage(const mtx::events::collections::TimelineEvents &event); + QString formatNotification(const mtx::responses::Notification ¬ification); - QString formatNotification(const mtx::events::collections::TimelineEvents &e); - -#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 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 &roomName, + const QString &sender, + const QString &text, + const QImage &icon); #endif // these slots are platform specific (D-Bus only) @@ -72,7 +98,7 @@ private slots: void notificationReplied(uint id, QString reply); }; -#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 9bcda1b2..ae6fdbee 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp @@ -12,6 +12,9 @@ #include +#include + +#include "Cache.h" #include "EventAccessors.h" #include "Utils.h" @@ -22,6 +25,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(); @@ -45,21 +60,32 @@ NotificationsManager::NotificationsManager(QObject *parent) SLOT(notificationReplied(uint, QString))); } -// SPDX-FileCopyrightText: 2012 Roland Hieber -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later +void +NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, + const QImage &icon) +{ + const auto room_id = QString::fromStdString(notification.room_id); + const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event)); + const auto room_name = + QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); + const auto text = formatNotification(notification); + systemPostNotification(room_id, event_id, room_name, text, icon); +} + +/** + * This function is based on code from + * https://github.com/rohieb/StratumsphereTrayIcon + * Copyright (C) 2012 Roland Hieber + * 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 &sender, const QString &text, const QImage &icon) { - Q_UNUSED(sender) - QVariantMap hints; hints["image-data"] = icon; hints["sound-name"] = "message-new-instant"; @@ -163,27 +189,46 @@ NotificationsManager::notificationClosed(uint id, uint reason) * specified at https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/Markup/ */ QString -NotificationsManager::formatNotification(const mtx::events::collections::TimelineEvents &e) +NotificationsManager::formatNotification(const mtx::responses::Notification ¬ification) { - static const auto hasMarkup = std::invoke([this]() -> bool { - for (auto x : dbus.call("GetCapabilities").arguments()) - if (x.toStringList().contains("body-markup")) - return true; - return false; - }); + const auto sender = + cache::displayName(QString::fromStdString(notification.room_id), + QString::fromStdString(mtx::accessors::sender(notification.event))); + const auto messageLeadIn = + ((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) + ? "* " + sender + " " + : sender + + (utils::isReply(notification.event) + ? tr(" replied", + "Used to denote that this message is a reply to another " + "message. Displayed as 'foo replied: message'.") + : "") + + ": "); - if (hasMarkup) - return mtx::accessors::formattedBodyWithFallback(e) + if (hasMarkup_) { + if (hasImages_ && + mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image) + return QString( + "\""") + .prepend(messageLeadIn); + + return mtx::accessors::formattedBodyWithFallback(notification.event) + .prepend(messageLeadIn) .replace("", "") .replace("", "") .replace("", "") .replace("", "") .replace(QRegularExpression("(.+\\<\\/mx-reply\\>)"), ""); + } return QTextDocumentFragment::fromHtml( - mtx::accessors::formattedBodyWithFallback(e).replace( - QRegularExpression("(.+\\<\\/mx-reply\\>)"), "")) - .toPlainText(); + mtx::accessors::formattedBodyWithFallback(notification.event) + .replace(QRegularExpression("(.+\\<\\/mx-reply\\>)"), "")) + .toPlainText() + .prepend(messageLeadIn); } /** diff --git a/src/notifications/ManagerMac.cpp b/src/notifications/ManagerMac.cpp index c9678638..12d8ab6f 100644 --- a/src/notifications/ManagerMac.cpp +++ b/src/notifications/ManagerMac.cpp @@ -3,14 +3,56 @@ #include #include +#include "Cache.h" #include "EventAccessors.h" #include "Utils.h" +#include + QString -NotificationsManager::formatNotification(const mtx::events::collections::TimelineEvents &e) +NotificationsManager::formatNotification(const mtx::responses::Notification ¬ification) { + const auto sender = + cache::displayName(QString::fromStdString(notification.room_id), + QString::fromStdString(mtx::accessors::sender(notification.event))); + return QTextDocumentFragment::fromHtml( - mtx::accessors::formattedBodyWithFallback(e).replace( - QRegularExpression("(.+\\<\\/mx-reply\\>)"), "")) - .toPlainText(); + mtx::accessors::formattedBodyWithFallback(notification.event) + .replace(QRegularExpression("(.+\\<\\/mx-reply\\>)"), "")) + .toPlainText() + .prepend((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) + ? "* " + sender + " " + : ""); +} + +void +NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, + 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 QString messageInfo = + QString("%1 %2 a message") + .arg(sender) + .arg((utils::isReply(notification.event) + ? tr("replied to", + "Used to denote that this message is a reply to another " + "message. Displayed as 'foo replied to a message'.") + : tr("sent", + "Used to denote that this message is a normal message. Displayed as 'foo " + "sent a message'."))); + + QString text = formatNotification(notification); + + QImage *image = nullptr; + if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image) + image = new QImage{cacheImage(notification.event)}; + + objCxxPostNotification(room_name, messageInfo, text, image); } diff --git a/src/notifications/ManagerMac.mm b/src/notifications/ManagerMac.mm index 3372c5af..226bcce0 100644 --- a/src/notifications/ManagerMac.mm +++ b/src/notifications/ManagerMac.mm @@ -1,7 +1,10 @@ #include "notifications/Manager.h" -#include +#import +#import + #include +#include @interface NSUserNotification (CFIPrivate) - (void)set_identityImage:(NSImage *)image; @@ -13,24 +16,22 @@ NotificationsManager::NotificationsManager(QObject *parent): QObject(parent) } void -NotificationsManager::systemPostNotification(const QString &room_id, - const QString &event_id, - const QString &roomName, - const QString &sender, - const QString &text, - const QImage &icon) +NotificationsManager::objCxxPostNotification(const QString &title, + const QString &subtitle, + const QString &informativeText, + const QImage *bodyImage) { - Q_UNUSED(room_id) - Q_UNUSED(event_id) - Q_UNUSED(icon) - NSUserNotification * notif = [[NSUserNotification alloc] init]; + NSUserNotification *notif = [[NSUserNotification alloc] init]; - notif.title = roomName.toNSString(); - notif.subtitle = QString("%1 sent a message").arg(sender).toNSString(); - notif.informativeText = text.toNSString(); + notif.title = title.toNSString(); + notif.subtitle = subtitle.toNSString(); + notif.informativeText = informativeText.toNSString(); notif.soundName = NSUserNotificationDefaultSoundName; + if (bodyImage != nullptr) + notif.contentImage = [[NSImage alloc] initWithCGImage: bodyImage->toCGImage() size: NSZeroSize]; + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif]; [notif autorelease]; } @@ -39,7 +40,7 @@ NotificationsManager::systemPostNotification(const QString &room_id, void NotificationsManager::actionInvoked(uint, QString) { - } +} void NotificationsManager::notificationReplied(uint, QString) diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp index 026c912f..b17c6e3b 100644 --- a/src/notifications/ManagerWin.cpp +++ b/src/notifications/ManagerWin.cpp @@ -6,8 +6,10 @@ #include "wintoastlib.h" #include +#include #include +#include "Cache.h" #include "EventAccessors.h" #include "Utils.h" @@ -42,17 +44,25 @@ NotificationsManager::NotificationsManager(QObject *parent) {} void -NotificationsManager::systemPostNotification(const QString &room_id, - const QString &event_id, - const QString &roomName, +NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, + const QImage &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 = formatNotification(notification); + + systemPostNotification(room_name, sender, text, icon); +} + +void +NotificationsManager::systemPostNotification(const QString &roomName, const QString &sender, const QString &text, const QImage &icon) { - Q_UNUSED(room_id) - Q_UNUSED(event_id) - Q_UNUSED(icon) - if (!isInitialized) init(); @@ -63,8 +73,11 @@ NotificationsManager::systemPostNotification(const QString &room_id, else templ.setTextField(sender.toStdWString(), WinToastTemplate::FirstLine); templ.setTextField(text.toStdWString(), WinToastTemplate::SecondLine); - // TODO: implement room or user avatar - // templ.setImagePath(L"C:/example.png"); + + auto iconPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + roomName + + "-room-avatar.png"; + if (icon.save(iconPath)) + templ.setImagePath(iconPath.toStdWString()); WinToast::instance()->showToast(templ, new CustomHandler()); } @@ -79,10 +92,17 @@ NotificationsManager::removeNotification(const QString &, const QString &) {} QString -NotificationsManager::formatNotification(const mtx::events::collections::TimelineEvents &e) +NotificationsManager::formatNotification(const mtx::responses::Notification ¬ification) { + const auto sender = + cache::displayName(QString::fromStdString(notification.room_id), + QString::fromStdString(mtx::accessors::sender(notification.event))); + return QTextDocumentFragment::fromHtml( - mtx::accessors::formattedBodyWithFallback(e).replace( - QRegularExpression("(.+\\<\\/mx-reply\\>)"), "")) - .toPlainText(); + mtx::accessors::formattedBodyWithFallback(notification.event) + .replace(QRegularExpression("(.+\\<\\/mx-reply\\>)"), "")) + .toPlainText() + .prepend((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) + ? "* " + sender + " " + : ""); }