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 "EventAccessors.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
#include <mtx/responses/notifications.hpp>
void
NotificationsManager::postNotification(const mtx::responses::Notification &notification,
const QImage &icon)
#include <QFile>
#include <QImage>
#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 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<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>
// 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
@ -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 &notification);
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<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
// 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 &

View File

@ -12,6 +12,9 @@
#include <functional>
#include <mtx/responses/notifications.hpp>
#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<QImage>();
@ -45,21 +60,32 @@ NotificationsManager::NotificationsManager(QObject *parent)
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
void
NotificationsManager::postNotification(const mtx::responses::Notification &notification,
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 <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 &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 &notification)
{
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(
"<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("<strong>", "<b>")
.replace("</strong>", "</b>")
.replace(QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), "");
}
return QTextDocumentFragment::fromHtml(
mtx::accessors::formattedBodyWithFallback(e).replace(
QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), ""))
.toPlainText();
mtx::accessors::formattedBodyWithFallback(notification.event)
.replace(QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), ""))
.toPlainText()
.prepend(messageLeadIn);
}
/**

View File

@ -3,14 +3,56 @@
#include <QRegularExpression>
#include <QTextDocumentFragment>
#include "Cache.h"
#include "EventAccessors.h"
#include "Utils.h"
#include <mtx/responses/notifications.hpp>
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(
mtx::accessors::formattedBodyWithFallback(e).replace(
QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), ""))
.toPlainText();
mtx::accessors::formattedBodyWithFallback(notification.event)
.replace(QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), ""))
.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 <Foundation/Foundation.h>
#import <Foundation/Foundation.h>
#import <AppKit/NSImage.h>
#include <QtMac>
#include <QImage>
@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)

View File

@ -6,8 +6,10 @@
#include "wintoastlib.h"
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTextDocumentFragment>
#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 &notification,
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 &notification)
{
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>.+\\<\\/mx-reply\\>)"), ""))
.toPlainText();
mtx::accessors::formattedBodyWithFallback(notification.event)
.replace(QRegularExpression("(<mx-reply>.+\\<\\/mx-reply\\>)"), ""))
.toPlainText()
.prepend((mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
? "* " + sender + " "
: "");
}