Turn NotificationData into a struct

This commit is contained in:
Ilya Fedin 2025-02-26 18:07:19 +00:00 committed by John Preston
parent b07d3c5403
commit a8d1eadfbf
2 changed files with 279 additions and 332 deletions

View file

@ -139,291 +139,6 @@ bool UseGNotification() {
return KSandbox::isFlatpak() && !ServiceRegistered;
}
class NotificationData final : public base::has_weak_ptr {
public:
using NotificationId = Window::Notifications::Manager::NotificationId;
using Info = Window::Notifications::NativeManager::NotificationInfo;
NotificationData(
not_null<Manager*> manager,
XdgNotifications::NotificationsProxy proxy,
NotificationId id,
const Info &info);
NotificationData(const NotificationData &other) = delete;
NotificationData &operator=(const NotificationData &other) = delete;
NotificationData(NotificationData &&other) = delete;
NotificationData &operator=(NotificationData &&other) = delete;
void show();
void close();
void setImage(QImage image);
private:
const not_null<Manager*> _manager;
NotificationId _id;
Media::Audio::LocalDiskCache _sounds;
XdgNotifications::NotificationsProxy _proxy;
XdgNotifications::Notifications _interface;
std::string _title;
std::string _body;
std::vector<gi::cstring> _actions;
GLib::VariantDict _hints;
std::string _imageKey;
uint _notificationId = 0;
rpl::lifetime _lifetime;
};
using Notification = std::unique_ptr<NotificationData>;
NotificationData::NotificationData(
not_null<Manager*> manager,
XdgNotifications::NotificationsProxy proxy,
NotificationId id,
const Info &info)
: _manager(manager)
, _id(id)
, _sounds(cWorkingDir() + u"tdata/audio_cache"_q)
, _proxy(proxy)
, _interface(proxy)
, _hints(GLib::VariantDict::new_())
, _imageKey(GetImageKey()) {
const auto &title = info.title;
const auto &subtitle = info.subtitle;
const auto &text = info.message;
if (HasCapability("body-markup")) {
_title = title.toStdString();
_body = subtitle.isEmpty()
? text.toHtmlEscaped().toStdString()
: u"<b>%1</b>\n%2"_q.arg(
subtitle.toHtmlEscaped(),
text.toHtmlEscaped()).toStdString();
} else {
_title = subtitle.isEmpty()
? title.toStdString()
: subtitle.toStdString() + " (" + title.toStdString() + ')';
_body = text.toStdString();
}
if (HasCapability("actions")) {
_actions.push_back("default");
_actions.push_back(tr::lng_open_link(tr::now).toStdString());
if (!info.options.hideMarkAsRead) {
// icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
_actions.push_back("mail-mark-read");
_actions.push_back(
tr::lng_context_mark_read(tr::now).toStdString());
}
if (HasCapability("inline-reply")
&& !info.options.hideReplyButton) {
_actions.push_back("inline-reply");
_actions.push_back(
tr::lng_notification_reply(tr::now).toStdString());
const auto notificationRepliedSignalId
= _interface.signal_notification_replied().connect([=](
XdgNotifications::Notifications,
uint id,
std::string text) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
if (id == _notificationId) {
_manager->notificationReplied(
_id,
{ QString::fromStdString(text), {} });
}
});
});
_lifetime.add([=] {
_interface.disconnect(notificationRepliedSignalId);
});
}
const auto actionInvokedSignalId
= _interface.signal_action_invoked().connect([=](
XdgNotifications::Notifications,
uint id,
std::string actionName) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
if (id == _notificationId) {
if (actionName == "default") {
_manager->notificationActivated(_id);
} else if (actionName == "mail-mark-read") {
_manager->notificationReplied(_id, {});
}
}
});
});
_lifetime.add([=] {
_interface.disconnect(actionInvokedSignalId);
});
const auto activationTokenSignalId
= _interface.signal_activation_token().connect([=](
XdgNotifications::Notifications,
uint id,
std::string token) {
if (id == _notificationId) {
GLib::setenv("XDG_ACTIVATION_TOKEN", token, true);
}
});
_lifetime.add([=] {
_interface.disconnect(activationTokenSignalId);
});
_actions.push_back({});
}
if (HasCapability("action-icons")) {
_hints.insert_value("action-icons", GLib::Variant::new_boolean(true));
}
if (HasCapability("sound")) {
const auto sound = info.sound
? info.sound()
: Media::Audio::LocalSound();
const auto path = sound
? _sounds.path(sound).toStdString()
: std::string();
if (!path.empty()) {
_hints.insert_value(
"sound-file",
GLib::Variant::new_string(path));
} else {
_hints.insert_value(
"suppress-sound",
GLib::Variant::new_boolean(true));
}
}
if (HasCapability("x-canonical-append")) {
_hints.insert_value(
"x-canonical-append",
GLib::Variant::new_string("true"));
}
_hints.insert_value("category", GLib::Variant::new_string("im.received"));
_hints.insert_value("desktop-entry", GLib::Variant::new_string(
QGuiApplication::desktopFileName().toStdString()));
const auto notificationClosedSignalId =
_interface.signal_notification_closed().connect([=](
XdgNotifications::Notifications,
uint id,
uint reason) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
/*
* From: https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html
* The reason the notification was closed
* 1 - The notification expired.
* 2 - The notification was dismissed by the user.
* 3 - The notification was closed by a call to CloseNotification.
* 4 - Undefined/reserved reasons.
*
* If the notification was dismissed by the user (reason == 2), the notification is not kept in notification history.
* We do not need to send a "CloseNotification" call later to clear it from history.
* Therefore we can drop the notification reference now.
* In all other cases we keep the notification reference so that we may clear the notification later from history,
* if the message for that notification is read (e.g. chat is opened or read from another device).
*/
if (id == _notificationId && reason == 2) {
_manager->clearNotification(_id);
}
});
});
_lifetime.add([=] {
_interface.disconnect(notificationClosedSignalId);
});
}
void NotificationData::show() {
// a hack for snap's activation restriction
const auto weak = base::make_weak(this);
StartServiceAsync(_proxy.get_connection(), crl::guard(weak, [=] {
const auto callbackWrap = gi::unwrap(
Gio::AsyncReadyCallback(
crl::guard(weak, [=](GObject::Object, Gio::AsyncResult res) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
const auto result = _interface.call_notify_finish(
res);
if (!result) {
Gio::DBusErrorNS_::strip_remote_error(
result.error());
LOG(("Native Notification Error: %1").arg(
result.error().message_().c_str()));
_manager->clearNotification(_id);
return;
}
_notificationId = std::get<1>(*result);
});
})),
gi::scope_async);
xdg_notifications_notifications_call_notify(
_interface.gobj_(),
AppName.data(),
0,
(_imageKey.empty() || !_hints.lookup_value(_imageKey)
? base::IconName().toStdString()
: std::string()).c_str(),
_title.c_str(),
_body.c_str(),
!_actions.empty()
? (_actions
| ranges::views::transform(&gi::cstring::c_str)
| ranges::to_vector).data()
: nullptr,
_hints.end().gobj_(),
-1,
nullptr,
&callbackWrap->wrapper,
callbackWrap);
}));
}
void NotificationData::close() {
_interface.call_close_notification(_notificationId, nullptr);
_manager->clearNotification(_id);
}
void NotificationData::setImage(QImage image) {
if (_imageKey.empty()) {
return;
}
image.convertTo(QImage::Format_RGBA8888);
_hints.insert_value(_imageKey, GLib::Variant::new_tuple({
GLib::Variant::new_int32(image.width()),
GLib::Variant::new_int32(image.height()),
GLib::Variant::new_int32(image.bytesPerLine()),
GLib::Variant::new_boolean(true),
GLib::Variant::new_int32(8),
GLib::Variant::new_int32(4),
GLib::Variant::new_from_data(
GLib::VariantType::new_("ay"),
reinterpret_cast<const uchar*>(image.constBits()),
image.sizeInBytes(),
true,
[image] {}),
}));
}
} // namespace
class Manager::Private {
@ -446,6 +161,12 @@ public:
~Private();
private:
struct NotificationData : public base::has_weak_ptr {
uint id = 0;
rpl::lifetime lifetime;
};
using Notification = std::unique_ptr<NotificationData>;
const not_null<Manager*> _manager;
base::flat_map<
@ -456,6 +177,7 @@ private:
Gio::Application _application;
XdgNotifications::NotificationsProxy _proxy;
XdgNotifications::Notifications _interface;
Media::Audio::LocalDiskCache _sounds;
rpl::lifetime _lifetime;
};
@ -606,7 +328,8 @@ Manager::Private::Private(not_null<Manager*> manager)
: _manager(manager)
, _application(UseGNotification()
? Gio::Application::get_default()
: nullptr) {
: nullptr)
, _sounds(cWorkingDir() + u"tdata/audio_cache"_q) {
const auto &serverInformation = CurrentServerInformation;
if (!serverInformation.name.empty()) {
@ -716,16 +439,11 @@ void Manager::Private::showNotification(
? info.title.toStdString()
: info.subtitle.toStdString()
+ " (" + info.title.toStdString() + ')'))
: std::variant<Notification, Gio::Notification>(
std::make_unique<NotificationData>(
_manager,
_proxy,
notificationId,
info));
if (const auto ptr = std::get_if<Gio::Notification>(&notification)) {
auto &notification = *ptr;
: std::variant<Notification, Gio::Notification>(Notification{});
std::vector<gi::cstring> actions;
auto hints = GLib::VariantDict::new_();
v::match(notification, [&](Gio::Notification &notification) {
notification.set_body(info.message.toStdString());
notification.set_icon(
@ -780,8 +498,157 @@ void Manager::Private::showNotification(
"app.notification-mark-as-read",
notificationVariant);
}
}
}, [&](const Notification &owned) {
const auto notification = owned.get();
if (HasCapability("actions")) {
actions.push_back("default");
actions.push_back(tr::lng_open_link(tr::now).toStdString());
if (!options.hideMarkAsRead) {
// icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
actions.push_back("mail-mark-read");
actions.push_back(
tr::lng_context_mark_read(tr::now).toStdString());
}
if (HasCapability("inline-reply")
&& !options.hideReplyButton) {
actions.push_back("inline-reply");
actions.push_back(
tr::lng_notification_reply(tr::now).toStdString());
const auto notificationRepliedSignalId
= _interface.signal_notification_replied().connect([=](
XdgNotifications::Notifications,
uint id,
std::string text) {
Core::Sandbox::Instance().customEnterFromEventLoop(
[&] {
if (id == notification->id) {
_manager->notificationReplied(
notificationId,
{ QString::fromStdString(text), {} });
}
});
});
notification->lifetime.add([=] {
_interface.disconnect(notificationRepliedSignalId);
});
}
const auto actionInvokedSignalId
= _interface.signal_action_invoked().connect([=](
XdgNotifications::Notifications,
uint id,
std::string actionName) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
if (id == notification->id) {
if (actionName == "default") {
_manager->notificationActivated(
notificationId);
} else if (actionName == "mail-mark-read") {
_manager->notificationReplied(
notificationId,
{});
}
}
});
});
notification->lifetime.add([=] {
_interface.disconnect(actionInvokedSignalId);
});
const auto activationTokenSignalId
= _interface.signal_activation_token().connect([=](
XdgNotifications::Notifications,
uint id,
std::string token) {
if (id == notification->id) {
GLib::setenv("XDG_ACTIVATION_TOKEN", token, true);
}
});
notification->lifetime.add([=] {
_interface.disconnect(activationTokenSignalId);
});
actions.push_back({});
}
if (HasCapability("action-icons")) {
hints.insert_value(
"action-icons",
GLib::Variant::new_boolean(true));
}
if (HasCapability("sound")) {
const auto sound = info.sound
? info.sound()
: Media::Audio::LocalSound();
const auto path = sound
? _sounds.path(sound).toStdString()
: std::string();
if (!path.empty()) {
hints.insert_value(
"sound-file",
GLib::Variant::new_string(path));
} else {
hints.insert_value(
"suppress-sound",
GLib::Variant::new_boolean(true));
}
}
if (HasCapability("x-canonical-append")) {
hints.insert_value(
"x-canonical-append",
GLib::Variant::new_string("true"));
}
hints.insert_value(
"category",
GLib::Variant::new_string("im.received"));
hints.insert_value("desktop-entry", GLib::Variant::new_string(
QGuiApplication::desktopFileName().toStdString()));
const auto notificationClosedSignalId =
_interface.signal_notification_closed().connect([=](
XdgNotifications::Notifications,
uint id,
uint reason) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
/*
* From: https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html
* The reason the notification was closed
* 1 - The notification expired.
* 2 - The notification was dismissed by the user.
* 3 - The notification was closed by a call to CloseNotification.
* 4 - Undefined/reserved reasons.
*
* If the notification was dismissed by the user (reason == 2), the notification is not kept in notification history.
* We do not need to send a "CloseNotification" call later to clear it from history.
* Therefore we can drop the notification reference now.
* In all other cases we keep the notification reference so that we may clear the notification later from history,
* if the message for that notification is read (e.g. chat is opened or read from another device).
*/
if (id == notification->id && reason == 2) {
clearNotification(notificationId);
}
});
});
notification->lifetime.add([=] {
_interface.disconnect(notificationClosedSignalId);
});
});
const auto imageKey = GetImageKey();
if (!options.hideNameAndPhoto) {
v::match(notification, [&](Gio::Notification &notification) {
QByteArray imageData;
@ -798,8 +665,29 @@ void Manager::Private::showNotification(
imageData.size(),
[imageData] {})));
}, [&](const Notification &notification) {
notification->setImage(
Window::Notifications::GenerateUserpic(peer, userpicView));
if (imageKey.empty()) {
return;
}
const auto image = Window::Notifications::GenerateUserpic(
peer,
userpicView
).convertToFormat(QImage::Format_RGBA8888);
hints.insert_value(imageKey, GLib::Variant::new_tuple({
GLib::Variant::new_int32(image.width()),
GLib::Variant::new_int32(image.height()),
GLib::Variant::new_int32(image.bytesPerLine()),
GLib::Variant::new_boolean(true),
GLib::Variant::new_int32(8),
GLib::Variant::new_int32(4),
GLib::Variant::new_from_data(
GLib::VariantType::new_("ay"),
reinterpret_cast<const uchar*>(image.constBits()),
image.sizeInBytes(),
true,
[image] {}),
}));
});
}
@ -812,10 +700,12 @@ void Manager::Private::showNotification(
v::match(oldNotification, [&](
const std::string &oldNotification) {
_application.withdraw_notification(oldNotification);
clearNotification(notificationId);
}, [&](const Notification &oldNotification) {
oldNotification->close();
_interface.call_close_notification(
oldNotification->id,
nullptr);
});
clearNotification(notificationId);
i = _notifications.find(key);
}
}
@ -833,23 +723,85 @@ void Manager::Private::showNotification(
const auto j = i->second.emplace(
info.itemId,
std::move(notification)).first;
v::get<Notification>(j->second)->show();
const auto weak = base::make_weak(
v::get<Notification>(j->second).get());
// work around snap's activation restriction
StartServiceAsync(
_proxy.get_connection(),
crl::guard(weak, [=]() mutable {
const auto hasBodyMarkup = HasCapability("body-markup");
const auto callbackWrap = gi::unwrap(
Gio::AsyncReadyCallback(
crl::guard(weak, [=](
GObject::Object,
Gio::AsyncResult res) {
auto &sandbox = Core::Sandbox::Instance();
sandbox.customEnterFromEventLoop([&] {
const auto result
= _interface.call_notify_finish(res);
if (!result) {
Gio::DBusErrorNS_::strip_remote_error(
result.error());
LOG(("Native Notification Error: %1").arg(
result.error().message_().c_str()));
clearNotification(notificationId);
return;
}
weak->id = std::get<1>(*result);
});
})),
gi::scope_async);
xdg_notifications_notifications_call_notify(
_interface.gobj_(),
AppName.data(),
0,
(imageKey.empty() || !hints.lookup_value(imageKey)
? base::IconName().toStdString()
: std::string()).c_str(),
(hasBodyMarkup || info.subtitle.isEmpty()
? info.title.toStdString()
: info.subtitle.toStdString()
+ " (" + info.title.toStdString() + ')').c_str(),
(hasBodyMarkup
? info.subtitle.isEmpty()
? info.message.toHtmlEscaped().toStdString()
: u"<b>%1</b>\n%2"_q.arg(
info.subtitle.toHtmlEscaped(),
info.message.toHtmlEscaped()).toStdString()
: info.message.toStdString()).c_str(),
!actions.empty()
? (actions
| ranges::views::transform(&gi::cstring::c_str)
| ranges::to_vector).data()
: nullptr,
hints.end().gobj_(),
-1,
nullptr,
&callbackWrap->wrapper,
callbackWrap);
}));
});
}
void Manager::Private::clearAll() {
for (const auto &[key, notifications] : base::take(_notifications)) {
for (const auto &[msgId, notification] : notifications) {
const auto notificationId = NotificationId{
.contextId = key,
.msgId = msgId,
};
v::match(notification, [&](const std::string &notification) {
const auto notificationId = NotificationId{
.contextId = key,
.msgId = msgId,
};
_application.withdraw_notification(notification);
clearNotification(notificationId);
}, [&](const Notification &notification) {
notification->close();
_interface.call_close_notification(notification->id, nullptr);
});
clearNotification(notificationId);
}
}
}
@ -879,10 +831,10 @@ void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
}
v::match(taken, [&](const std::string &taken) {
_application.withdraw_notification(taken);
clearNotification(notificationId);
}, [&](const Notification &taken) {
taken->close();
_interface.call_close_notification(taken->id, nullptr);
});
clearNotification(notificationId);
}
void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
@ -896,16 +848,16 @@ void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
_notifications.erase(i);
for (const auto &[msgId, notification] : temp) {
const auto notificationId = NotificationId{
.contextId = key,
.msgId = msgId,
};
v::match(notification, [&](const std::string &notification) {
const auto notificationId = NotificationId{
.contextId = key,
.msgId = msgId,
};
_application.withdraw_notification(notification);
clearNotification(notificationId);
}, [&](const Notification &notification) {
notification->close();
_interface.call_close_notification(notification->id, nullptr);
});
clearNotification(notificationId);
}
}
}
@ -925,16 +877,16 @@ void Manager::Private::clearFromHistory(not_null<History*> history) {
i = _notifications.erase(i);
for (const auto &[msgId, notification] : temp) {
const auto notificationId = NotificationId{
.contextId = key,
.msgId = msgId,
};
v::match(notification, [&](const std::string &notification) {
const auto notificationId = NotificationId{
.contextId = key,
.msgId = msgId,
};
_application.withdraw_notification(notification);
clearNotification(notificationId);
}, [&](const Notification &notification) {
notification->close();
_interface.call_close_notification(notification->id, nullptr);
});
clearNotification(notificationId);
}
}
}
@ -950,16 +902,16 @@ void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
i = _notifications.erase(i);
for (const auto &[msgId, notification] : temp) {
const auto notificationId = NotificationId{
.contextId = key,
.msgId = msgId,
};
v::match(notification, [&](const std::string &notification) {
const auto notificationId = NotificationId{
.contextId = key,
.msgId = msgId,
};
_application.withdraw_notification(notification);
clearNotification(notificationId);
}, [&](const Notification &notification) {
notification->close();
_interface.call_close_notification(notification->id, nullptr);
});
clearNotification(notificationId);
}
}
}
@ -988,10 +940,6 @@ Manager::Manager(not_null<Window::Notifications::System*> system)
, _private(std::make_unique<Private>(this)) {
}
void Manager::clearNotification(NotificationId id) {
_private->clearNotification(id);
}
Manager::~Manager() = default;
void Manager::doShowNativeNotification(

View file

@ -15,7 +15,6 @@ namespace Notifications {
class Manager : public Window::Notifications::NativeManager {
public:
Manager(not_null<Window::Notifications::System*> system);
void clearNotification(NotificationId id);
~Manager();
protected: