Rewrite notification posting logic

This does away with the nice abstraction layers in order to easily get the best-looking notifications for each platform.
This commit is contained in:
Nicolas Werner 2021-03-17 19:17:57 +01:00
parent 37acdad928
commit f578272a0d
No known key found for this signature in database
GPG Key ID: C8D75E610773F2D9
6 changed files with 261 additions and 83 deletions

View File

@ -2,32 +2,76 @@
#include "Cache.h" #include "Cache.h"
#include "EventAccessors.h" #include "EventAccessors.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h" #include "Utils.h"
#include <mtx/responses/notifications.hpp>
void #include <QFile>
NotificationsManager::postNotification(const mtx::responses::Notification &notification, #include <QImage>
const QImage &icon) #include <QStandardPaths>
#include <mtxclient/crypto/client.hpp>
QString
NotificationsManager::cacheImage(const mtx::events::collections::TimelineEvents &event)
{ {
const auto room_id = QString::fromStdString(notification.room_id); const auto url = mtx::accessors::url(event);
const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event)); auto encryptionInfo = mtx::accessors::file(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 QString reply = (utils::isReply(notification.event) auto filename = QString::fromStdString(mtx::accessors::body(event));
? tr(" replied", QString path{QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" +
"Used to denote that this message is a reply to another " filename};
"message. Displayed as 'foo replied: message'.")
: "");
// the "replied" is only added if this message is not an emote message http::client()->download(
QString text = url,
((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) [path, url, encryptionInfo](const std::string &data,
? "* " + sender + " " const std::string &,
: sender + reply + ": ") + const std::string &,
formatNotification(notification.event); mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(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();
} }

View File

@ -10,7 +10,12 @@
#include <mtx/responses/notifications.hpp> #include <mtx/responses/notifications.hpp>
// convenience definition
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) #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/QDBusArgument>
#include <QtDBus/QDBusInterface> #include <QtDBus/QDBusInterface>
#endif #endif
@ -43,25 +48,46 @@ public slots:
void removeNotification(const QString &roomId, const QString &eventId); void removeNotification(const QString &roomId, const QString &eventId);
private: private:
void systemPostNotification(const QString &room_id, QString cacheImage(const mtx::events::collections::TimelineEvents &event);
const QString &event_id, QString formatNotification(const mtx::responses::Notification &notification);
const QString &roomName,
const QString &sender,
const QString &text,
const QImage &icon);
QString formatNotification(const mtx::events::collections::TimelineEvents &e); #if defined(NHEKO_DBUS_SYS)
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU)
public: public:
void closeNotifications(QString roomId); void closeNotifications(QString roomId);
private: private:
QDBusInterface dbus; 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); void closeNotification(uint id);
// notification ID to (room ID, event ID) // notification ID to (room ID, event ID)
QMap<uint, roomEventId> notificationIds; 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 &roomName,
const QString &sender,
const QString &text,
const QImage &icon);
#endif #endif
// these slots are platform specific (D-Bus only) // these slots are platform specific (D-Bus only)
@ -72,7 +98,7 @@ private slots:
void notificationReplied(uint id, QString reply); 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 & QDBusArgument &
operator<<(QDBusArgument &arg, const QImage &image); operator<<(QDBusArgument &arg, const QImage &image);
const QDBusArgument & const QDBusArgument &

View File

@ -12,6 +12,9 @@
#include <functional> #include <functional>
#include <mtx/responses/notifications.hpp>
#include "Cache.h"
#include "EventAccessors.h" #include "EventAccessors.h"
#include "Utils.h" #include "Utils.h"
@ -22,6 +25,18 @@ NotificationsManager::NotificationsManager(QObject *parent)
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
QDBusConnection::sessionBus(), QDBusConnection::sessionBus(),
this) 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>(); qDBusRegisterMetaType<QImage>();
@ -45,21 +60,32 @@ NotificationsManager::NotificationsManager(QObject *parent)
SLOT(notificationReplied(uint, QString))); SLOT(notificationReplied(uint, QString)));
} }
// SPDX-FileCopyrightText: 2012 Roland Hieber <rohieb@rohieb.name> void
// SPDX-FileCopyrightText: 2021 Nheko Contributors NotificationsManager::postNotification(const mtx::responses::Notification &notification,
// const QImage &icon)
// SPDX-License-Identifier: GPL-3.0-or-later {
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 <rohieb@rohieb.name>
* Licensed under the GNU General Public License, version 3
*/
void void
NotificationsManager::systemPostNotification(const QString &room_id, NotificationsManager::systemPostNotification(const QString &room_id,
const QString &event_id, const QString &event_id,
const QString &roomName, const QString &roomName,
const QString &sender,
const QString &text, const QString &text,
const QImage &icon) const QImage &icon)
{ {
Q_UNUSED(sender)
QVariantMap hints; QVariantMap hints;
hints["image-data"] = icon; hints["image-data"] = icon;
hints["sound-name"] = "message-new-instant"; 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/ * specified at https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/Markup/
*/ */
QString QString
NotificationsManager::formatNotification(const mtx::events::collections::TimelineEvents &e) NotificationsManager::formatNotification(const mtx::responses::Notification &notification)
{ {
static const auto hasMarkup = std::invoke([this]() -> bool { const auto sender =
for (auto x : dbus.call("GetCapabilities").arguments()) cache::displayName(QString::fromStdString(notification.room_id),
if (x.toStringList().contains("body-markup")) QString::fromStdString(mtx::accessors::sender(notification.event)));
return true; const auto messageLeadIn =
return false; ((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) if (hasMarkup_) {
return mtx::accessors::formattedBodyWithFallback(e) if (hasImages_ &&
mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image)
return QString(
"<img src=\"file:///" + cacheImage(notification.event) +
"\" alt=\"" +
mtx::accessors::formattedBodyWithFallback(notification.event) +
"\">")
.prepend(messageLeadIn);
return mtx::accessors::formattedBodyWithFallback(notification.event)
.prepend(messageLeadIn)
.replace("<em>", "<i>") .replace("<em>", "<i>")
.replace("</em>", "</i>") .replace("</em>", "</i>")
.replace("<strong>", "<b>") .replace("<strong>", "<b>")
.replace("</strong>", "</b>") .replace("</strong>", "</b>")
.replace(QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), ""); .replace(QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), "");
}
return QTextDocumentFragment::fromHtml( return QTextDocumentFragment::fromHtml(
mtx::accessors::formattedBodyWithFallback(e).replace( mtx::accessors::formattedBodyWithFallback(notification.event)
QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), "")) .replace(QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), ""))
.toPlainText(); .toPlainText()
.prepend(messageLeadIn);
} }
/** /**

View File

@ -3,14 +3,56 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QTextDocumentFragment> #include <QTextDocumentFragment>
#include "Cache.h"
#include "EventAccessors.h" #include "EventAccessors.h"
#include "Utils.h" #include "Utils.h"
#include <mtx/responses/notifications.hpp>
QString QString
NotificationsManager::formatNotification(const mtx::events::collections::TimelineEvents &e) NotificationsManager::formatNotification(const mtx::responses::Notification &notification)
{ {
const auto sender =
cache::displayName(QString::fromStdString(notification.room_id),
QString::fromStdString(mtx::accessors::sender(notification.event)));
return QTextDocumentFragment::fromHtml( return QTextDocumentFragment::fromHtml(
mtx::accessors::formattedBodyWithFallback(e).replace( mtx::accessors::formattedBodyWithFallback(notification.event)
QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), "")) .replace(QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), ""))
.toPlainText(); .toPlainText()
.prepend((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
? "* " + sender + " "
: "");
}
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 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);
} }

View File

@ -1,7 +1,10 @@
#include "notifications/Manager.h" #include "notifications/Manager.h"
#include <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <AppKit/NSImage.h>
#include <QtMac> #include <QtMac>
#include <QImage>
@interface NSUserNotification (CFIPrivate) @interface NSUserNotification (CFIPrivate)
- (void)set_identityImage:(NSImage *)image; - (void)set_identityImage:(NSImage *)image;
@ -13,24 +16,22 @@ NotificationsManager::NotificationsManager(QObject *parent): QObject(parent)
} }
void void
NotificationsManager::systemPostNotification(const QString &room_id, NotificationsManager::objCxxPostNotification(const QString &title,
const QString &event_id, const QString &subtitle,
const QString &roomName, const QString &informativeText,
const QString &sender, const QImage *bodyImage)
const QString &text,
const QImage &icon)
{ {
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.title = title.toNSString();
notif.subtitle = QString("%1 sent a message").arg(sender).toNSString(); notif.subtitle = subtitle.toNSString();
notif.informativeText = text.toNSString(); notif.informativeText = informativeText.toNSString();
notif.soundName = NSUserNotificationDefaultSoundName; notif.soundName = NSUserNotificationDefaultSoundName;
if (bodyImage != nullptr)
notif.contentImage = [[NSImage alloc] initWithCGImage: bodyImage->toCGImage() size: NSZeroSize];
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif]; [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif];
[notif autorelease]; [notif autorelease];
} }
@ -39,7 +40,7 @@ NotificationsManager::systemPostNotification(const QString &room_id,
void void
NotificationsManager::actionInvoked(uint, QString) NotificationsManager::actionInvoked(uint, QString)
{ {
} }
void void
NotificationsManager::notificationReplied(uint, QString) NotificationsManager::notificationReplied(uint, QString)

View File

@ -6,8 +6,10 @@
#include "wintoastlib.h" #include "wintoastlib.h"
#include <QRegularExpression> #include <QRegularExpression>
#include <QStandardPaths>
#include <QTextDocumentFragment> #include <QTextDocumentFragment>
#include "Cache.h"
#include "EventAccessors.h" #include "EventAccessors.h"
#include "Utils.h" #include "Utils.h"
@ -42,17 +44,25 @@ NotificationsManager::NotificationsManager(QObject *parent)
{} {}
void void
NotificationsManager::systemPostNotification(const QString &room_id, NotificationsManager::postNotification(const mtx::responses::Notification &notification,
const QString &event_id, const QImage &icon)
const QString &roomName, {
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 &sender,
const QString &text, const QString &text,
const QImage &icon) const QImage &icon)
{ {
Q_UNUSED(room_id)
Q_UNUSED(event_id)
Q_UNUSED(icon)
if (!isInitialized) if (!isInitialized)
init(); init();
@ -63,8 +73,11 @@ NotificationsManager::systemPostNotification(const QString &room_id,
else else
templ.setTextField(sender.toStdWString(), WinToastTemplate::FirstLine); templ.setTextField(sender.toStdWString(), WinToastTemplate::FirstLine);
templ.setTextField(text.toStdWString(), WinToastTemplate::SecondLine); 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()); WinToast::instance()->showToast(templ, new CustomHandler());
} }
@ -79,10 +92,17 @@ NotificationsManager::removeNotification(const QString &, const QString &)
{} {}
QString QString
NotificationsManager::formatNotification(const mtx::events::collections::TimelineEvents &e) NotificationsManager::formatNotification(const mtx::responses::Notification &notification)
{ {
const auto sender =
cache::displayName(QString::fromStdString(notification.room_id),
QString::fromStdString(mtx::accessors::sender(notification.event)));
return QTextDocumentFragment::fromHtml( return QTextDocumentFragment::fromHtml(
mtx::accessors::formattedBodyWithFallback(e).replace( mtx::accessors::formattedBodyWithFallback(notification.event)
QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), "")) .replace(QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), ""))
.toPlainText(); .toPlainText()
.prepend((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
? "* " + sender + " "
: "");
} }