diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index c8561ba18..4028f4f52 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -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, - 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; - NotificationId _id; - - Media::Audio::LocalDiskCache _sounds; - - XdgNotifications::NotificationsProxy _proxy; - XdgNotifications::Notifications _interface; - std::string _title; - std::string _body; - std::vector _actions; - GLib::VariantDict _hints; - std::string _imageKey; - - uint _notificationId = 0; - rpl::lifetime _lifetime; - -}; - -using Notification = std::unique_ptr; - -NotificationData::NotificationData( - not_null 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"%1\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(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; + const not_null _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) , _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( - std::make_unique( - _manager, - _proxy, - notificationId, - info)); - - if (const auto ptr = std::get_if(¬ification)) { - auto ¬ification = *ptr; + : std::variant(Notification{}); + std::vector actions; + auto hints = GLib::VariantDict::new_(); + v::match(notification, [&](Gio::Notification ¬ification) { 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 ¬ification) { QByteArray imageData; @@ -798,8 +665,29 @@ void Manager::Private::showNotification( imageData.size(), [imageData] {}))); }, [&](const Notification ¬ification) { - 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(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(j->second)->show(); + + const auto weak = base::make_weak( + v::get(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"%1\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 ¬ification) { - const auto notificationId = NotificationId{ - .contextId = key, - .msgId = msgId, - }; _application.withdraw_notification(notification); - clearNotification(notificationId); }, [&](const Notification ¬ification) { - notification->close(); + _interface.call_close_notification(notification->id, nullptr); }); + clearNotification(notificationId); } } } @@ -879,10 +831,10 @@ void Manager::Private::clearFromItem(not_null 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 topic) { @@ -896,16 +848,16 @@ void Manager::Private::clearFromTopic(not_null topic) { _notifications.erase(i); for (const auto &[msgId, notification] : temp) { + const auto notificationId = NotificationId{ + .contextId = key, + .msgId = msgId, + }; v::match(notification, [&](const std::string ¬ification) { - const auto notificationId = NotificationId{ - .contextId = key, - .msgId = msgId, - }; _application.withdraw_notification(notification); - clearNotification(notificationId); }, [&](const Notification ¬ification) { - notification->close(); + _interface.call_close_notification(notification->id, nullptr); }); + clearNotification(notificationId); } } } @@ -925,16 +877,16 @@ void Manager::Private::clearFromHistory(not_null 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 ¬ification) { - const auto notificationId = NotificationId{ - .contextId = key, - .msgId = msgId, - }; _application.withdraw_notification(notification); - clearNotification(notificationId); }, [&](const Notification ¬ification) { - notification->close(); + _interface.call_close_notification(notification->id, nullptr); }); + clearNotification(notificationId); } } } @@ -950,16 +902,16 @@ void Manager::Private::clearFromSession(not_null 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 ¬ification) { - const auto notificationId = NotificationId{ - .contextId = key, - .msgId = msgId, - }; _application.withdraw_notification(notification); - clearNotification(notificationId); }, [&](const Notification ¬ification) { - notification->close(); + _interface.call_close_notification(notification->id, nullptr); }); + clearNotification(notificationId); } } } @@ -988,10 +940,6 @@ Manager::Manager(not_null system) , _private(std::make_unique(this)) { } -void Manager::clearNotification(NotificationId id) { - _private->clearNotification(id); -} - Manager::~Manager() = default; void Manager::doShowNativeNotification( diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h index ecf1ce0c0..8ab17f55b 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h @@ -15,7 +15,6 @@ namespace Notifications { class Manager : public Window::Notifications::NativeManager { public: Manager(not_null system); - void clearNotification(NotificationId id); ~Manager(); protected: