/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "platform/linux/notifications_manager_linux.h" #include "base/options.h" #include "base/platform/base_platform_info.h" #include "base/platform/linux/base_linux_glibmm_helper.h" #include "base/platform/linux/base_linux_dbus_utilities.h" #include "platform/platform_specific.h" #include "core/application.h" #include "core/sandbox.h" #include "core/core_settings.h" #include "data/data_forum_topic.h" #include "history/history.h" #include "history/history_item.h" #include "ui/empty_userpic.h" #include "main/main_session.h" #include "lang/lang_keys.h" #include "base/weak_ptr.h" #include "window/notifications_utilities.h" #include "styles/style_window.h" #include #include #include #include #include #include namespace Platform { namespace Notifications { namespace { constexpr auto kService = "org.freedesktop.Notifications"_cs; constexpr auto kObjectPath = "/org/freedesktop/Notifications"_cs; constexpr auto kInterface = kService; constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs; using namespace base::Platform; struct ServerInformation { QString name; QString vendor; QVersionNumber version; QVersionNumber specVersion; }; bool ServiceRegistered = false; std::optional CurrentServerInformation; QStringList CurrentCapabilities; void Noexcept(Fn callback, Fn failed = nullptr) noexcept { try { callback(); return; } catch (const std::exception &e) { LOG(("Native Notification Error: %1").arg( QString::fromStdString(e.what()))); } if (failed) { failed(); } } std::unique_ptr CreateServiceWatcher() { try { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); const auto activatable = [&] { try { return ranges::contains( base::Platform::DBus::ListActivatableNames(connection), Glib::ustring(std::string(kService))); } catch (...) { // avoid service restart loop in sandboxed environments return true; } }(); return std::make_unique( connection, std::string(kService), [=]( const Glib::ustring &service, const Glib::ustring &oldOwner, const Glib::ustring &newOwner) { if (activatable && newOwner.empty()) { crl::on_main([] { Core::App().notifications().clearAll(); }); return; } crl::on_main([] { Core::App().notifications().createManager(); }); }); } catch (...) { } return nullptr; } void StartServiceAsync(Fn callback) { try { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); DBus::StartServiceByNameAsync( connection, std::string(kService), [=](Fn result) { Noexcept([&] { try { result(); // get the error if any } catch (const Glib::Error &e) { static const auto NotSupportedErrors = { "org.freedesktop.DBus.Error.ServiceUnknown", }; const auto errorName = Gio::DBus::ErrorUtils::get_remote_error(e); if (!ranges::contains(NotSupportedErrors, errorName)) { throw e; } } }); crl::on_main(callback); }); return; } catch (...) { } crl::on_main(callback); } bool GetServiceRegistered() { try { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); const auto hasOwner = [&] { try { return DBus::NameHasOwner( connection, std::string(kService)); } catch (...) { return false; } }(); static const auto activatable = [&] { try { return ranges::contains( DBus::ListActivatableNames(connection), Glib::ustring(std::string(kService))); } catch (...) { return false; } }(); return hasOwner || activatable; } catch (...) { } return false; } void GetServerInformation( Fn &)> callback) { Noexcept([&] { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); connection->call( std::string(kObjectPath), std::string(kInterface), "GetServerInformation", {}, [=](const Glib::RefPtr &result) { Noexcept([&] { auto reply = connection->call_finish(result); const auto name = GlibVariantCast( reply.get_child(0)); const auto vendor = GlibVariantCast( reply.get_child(1)); const auto version = GlibVariantCast( reply.get_child(2)); const auto specVersion = GlibVariantCast( reply.get_child(3)); crl::on_main([=] { callback(ServerInformation{ QString::fromStdString(name), QString::fromStdString(vendor), QVersionNumber::fromString( QString::fromStdString(version)), QVersionNumber::fromString( QString::fromStdString(specVersion)), }); }); }, [&] { crl::on_main([=] { callback(std::nullopt); }); }); }, std::string(kService)); }, [&] { crl::on_main([=] { callback(std::nullopt); }); }); } void GetCapabilities(Fn callback) { Noexcept([&] { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); connection->call( std::string(kObjectPath), std::string(kInterface), "GetCapabilities", {}, [=](const Glib::RefPtr &result) { Noexcept([&] { auto reply = connection->call_finish(result); QStringList value; ranges::transform( GlibVariantCast>( reply.get_child(0)), ranges::back_inserter(value), QString::fromStdString); crl::on_main([=] { callback(value); }); }, [&] { crl::on_main([=] { callback({}); }); }); }, std::string(kService)); }, [&] { crl::on_main([=] { callback({}); }); }); } void GetInhibited(Fn callback) { Noexcept([&] { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); connection->call( std::string(kObjectPath), std::string(kPropertiesInterface), "Get", MakeGlibVariant(std::tuple{ Glib::ustring(std::string(kInterface)), Glib::ustring("Inhibited"), }), [=](const Glib::RefPtr &result) { Noexcept([&] { auto reply = connection->call_finish(result); const auto value = GlibVariantCast( GlibVariantCast( reply.get_child(0))); crl::on_main([=] { callback(value); }); }, [&] { crl::on_main([=] { callback(false); }); }); }, std::string(kService)); }, [&] { crl::on_main([=] { callback(false); }); }); } ServerInformation CurrentServerInformationValue() { return CurrentServerInformation.value_or(ServerInformation{}); } Glib::ustring GetImageKey(const QVersionNumber &specificationVersion) { const auto normalizedVersion = specificationVersion.normalized(); if (normalizedVersion.isNull()) { LOG(("Native Notification Error: specification version is null")); return {}; } if (normalizedVersion >= QVersionNumber(1, 2)) { return "image-data"; } else if (normalizedVersion == QVersionNumber(1, 1)) { return "image_data"; } return "icon_data"; } class NotificationData final : public base::has_weak_ptr { public: using NotificationId = Window::Notifications::Manager::NotificationId; NotificationData( not_null manager, NotificationId id); [[nodiscard]] bool init( const QString &title, const QString &subtitle, const QString &msg, Window::Notifications::Manager::DisplayOptions options); NotificationData(const NotificationData &other) = delete; NotificationData &operator=(const NotificationData &other) = delete; NotificationData(NotificationData &&other) = delete; NotificationData &operator=(NotificationData &&other) = delete; ~NotificationData(); void show(); void close(); void setImage(const QImage &image); private: const not_null _manager; NotificationId _id; Glib::RefPtr _application; Glib::RefPtr _notification; Glib::RefPtr _dbusConnection; Glib::ustring _title; Glib::ustring _body; std::vector _actions; std::map _hints; Glib::ustring _imageKey; uint _notificationId = 0; uint _actionInvokedSignalId = 0; uint _activationTokenSignalId = 0; uint _notificationRepliedSignalId = 0; uint _notificationClosedSignalId = 0; void notificationClosed(uint id, uint reason); void actionInvoked(uint id, const Glib::ustring &actionName); void activationToken(uint id, const Glib::ustring &token); void notificationReplied(uint id, const Glib::ustring &text); }; using Notification = std::unique_ptr; NotificationData::NotificationData( not_null manager, NotificationId id) : _manager(manager) , _id(id) , _application(Gio::Application::get_default()) { } bool NotificationData::init( const QString &title, const QString &subtitle, const QString &msg, Window::Notifications::Manager::DisplayOptions options) { if (_application) { _notification = Gio::Notification::create(title.toStdString()); _notification->set_body( subtitle.isEmpty() ? msg.toStdString() : u"%1\n%2"_q.arg(subtitle, msg).toStdString()); _notification->set_icon( Gio::ThemedIcon::create(base::IconName().toStdString())); // glib 2.42+, we keep glib 2.40+ compatibility static const auto set_priority = [] { // reset dlerror after dlsym call const auto guard = gsl::finally([] { dlerror(); }); return reinterpret_cast( dlsym(RTLD_DEFAULT, "g_notification_set_priority")); }(); if (set_priority) { // for chat messages, according to // https://docs.gtk.org/gio/enum.NotificationPriority.html set_priority(_notification->gobj(), G_NOTIFICATION_PRIORITY_HIGH); } // glib 2.70+, we keep glib 2.40+ compatibility static const auto set_category = [] { // reset dlerror after dlsym call const auto guard = gsl::finally([] { dlerror(); }); return reinterpret_cast( dlsym(RTLD_DEFAULT, "g_notification_set_category")); }(); if (set_category) { set_category(_notification->gobj(), "im.received"); } const auto idTuple = _id.toTuple(); _notification->set_default_action( "app.notification-reply", idTuple); if (!options.hideMarkAsRead) { _notification->add_button( tr::lng_context_mark_read(tr::now).toStdString(), "app.notification-mark-as-read", idTuple); } if (!options.hideReplyButton) { _notification->add_button( tr::lng_notification_reply(tr::now).toStdString(), "app.notification-reply", idTuple); } return true; } Noexcept([&] { _dbusConnection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); }); if (!_dbusConnection) { return false; } const auto weak = base::make_weak(this); const auto capabilities = CurrentCapabilities; const auto signalEmitted = [=]( const Glib::RefPtr &connection, const Glib::ustring &sender_name, const Glib::ustring &object_path, const Glib::ustring &interface_name, const Glib::ustring &signal_name, Glib::VariantContainerBase parameters) { Noexcept([&] { if (signal_name == "ActionInvoked") { const auto id = GlibVariantCast( parameters.get_child(0)); const auto actionName = GlibVariantCast( parameters.get_child(1)); crl::on_main(weak, [=] { actionInvoked(id, actionName); }); } else if (signal_name == "ActivationToken") { const auto id = GlibVariantCast( parameters.get_child(0)); const auto token = GlibVariantCast( parameters.get_child(1)); crl::on_main(weak, [=] { activationToken(id, token); }); } else if (signal_name == "NotificationReplied") { const auto id = GlibVariantCast( parameters.get_child(0)); const auto text = GlibVariantCast( parameters.get_child(1)); crl::on_main(weak, [=] { notificationReplied(id, text); }); } else if (signal_name == "NotificationClosed") { const auto id = GlibVariantCast( parameters.get_child(0)); const auto reason = GlibVariantCast( parameters.get_child(1)); crl::on_main(weak, [=] { notificationClosed(id, reason); }); } }); }; _title = title.toStdString(); _imageKey = GetImageKey(CurrentServerInformationValue().specVersion); if (capabilities.contains(u"body-markup"_q)) { _body = subtitle.isEmpty() ? msg.toHtmlEscaped().toStdString() : u"%1\n%2"_q.arg( subtitle.toHtmlEscaped(), msg.toHtmlEscaped()).toStdString(); } else { _body = subtitle.isEmpty() ? msg.toStdString() : u"%1\n%2"_q.arg(subtitle, msg).toStdString(); } if (capabilities.contains("actions")) { _actions.push_back("default"); _actions.push_back({}); if (!options.hideMarkAsRead) { _actions.push_back("mail-mark-read"); _actions.push_back( tr::lng_context_mark_read(tr::now).toStdString()); } if (!options.hideReplyButton) { if (capabilities.contains("inline-reply")) { _actions.push_back("inline-reply"); _actions.push_back( tr::lng_notification_reply(tr::now).toStdString()); _notificationRepliedSignalId = _dbusConnection->signal_subscribe( signalEmitted, std::string(kService), std::string(kInterface), "NotificationReplied", std::string(kObjectPath)); } else { // icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html _actions.push_back("mail-reply-sender"); _actions.push_back( tr::lng_notification_reply(tr::now).toStdString()); } } _actionInvokedSignalId = _dbusConnection->signal_subscribe( signalEmitted, std::string(kService), std::string(kInterface), "ActionInvoked", std::string(kObjectPath)); _activationTokenSignalId = _dbusConnection->signal_subscribe( signalEmitted, std::string(kService), std::string(kInterface), "ActivationToken", std::string(kObjectPath)); } if (capabilities.contains("action-icons")) { _hints["action-icons"] = Glib::Variant::create(true); } // suppress system sound if telegram sound activated, // otherwise use system sound if (capabilities.contains("sound")) { if (Core::App().settings().soundNotify()) { _hints["suppress-sound"] = Glib::Variant::create(true); } else { // sound name according to http://0pointer.de/public/sound-naming-spec.html _hints["sound-name"] = Glib::Variant::create( "message-new-instant"); } } if (capabilities.contains("x-canonical-append")) { _hints["x-canonical-append"] = Glib::Variant::create( "true"); } _hints["category"] = Glib::Variant::create("im.received"); _hints["desktop-entry"] = Glib::Variant::create( QGuiApplication::desktopFileName().chopped(8).toStdString()); _notificationClosedSignalId = _dbusConnection->signal_subscribe( signalEmitted, std::string(kService), std::string(kInterface), "NotificationClosed", std::string(kObjectPath)); return true; } NotificationData::~NotificationData() { if (_dbusConnection) { if (_actionInvokedSignalId != 0) { _dbusConnection->signal_unsubscribe(_actionInvokedSignalId); } if (_activationTokenSignalId != 0) { _dbusConnection->signal_unsubscribe(_activationTokenSignalId); } if (_notificationRepliedSignalId != 0) { _dbusConnection->signal_unsubscribe(_notificationRepliedSignalId); } if (_notificationClosedSignalId != 0) { _dbusConnection->signal_unsubscribe(_notificationClosedSignalId); } } } void NotificationData::show() { if (_application && _notification) { _application->send_notification( std::to_string(_id.contextId.sessionId) + '-' + std::to_string(_id.contextId.peerId.value) + '-' + std::to_string(_id.msgId.bare), _notification); return; } // a hack for snap's activation restriction const auto weak = base::make_weak(this); StartServiceAsync(crl::guard(weak, [=] { const auto iconName = _imageKey.empty() || _hints.find(_imageKey) == end(_hints) ? Glib::ustring(base::IconName().toStdString()) : Glib::ustring(); const auto connection = _dbusConnection; connection->call( std::string(kObjectPath), std::string(kInterface), "Notify", MakeGlibVariant(std::tuple{ Glib::ustring(std::string(AppName)), uint(0), iconName, _title, _body, _actions, _hints, -1, }), [=](const Glib::RefPtr &result) { Noexcept([&] { auto reply = connection->call_finish(result); const auto notificationId = GlibVariantCast( reply.get_child(0)); crl::on_main(weak, [=] { _notificationId = notificationId; }); }, [&] { crl::on_main(weak, [=] { _manager->clearNotification(_id); }); }); }, std::string(kService)); })); } void NotificationData::close() { if (_application) { _application->withdraw_notification( std::to_string(_id.contextId.sessionId) + '-' + std::to_string(_id.contextId.peerId.value) + '-' + std::to_string(_id.msgId.bare)); _manager->clearNotification(_id); return; } _dbusConnection->call( std::string(kObjectPath), std::string(kInterface), "CloseNotification", MakeGlibVariant(std::tuple{ _notificationId, }), {}, std::string(kService), -1, Gio::DBus::CallFlags::NO_AUTO_START); _manager->clearNotification(_id); } void NotificationData::setImage(const QImage &image) { if (_notification) { const auto imageData = [&] { QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); return ba; }(); const auto imageBytes = Glib::Bytes::create( imageData.constData(), imageData.size()); _notification->set_icon( Glib::wrap(g_bytes_icon_new(imageBytes->gobj()))); return; } if (_imageKey.empty()) { return; } const auto convertedImage = image.hasAlphaChannel() ? image.convertToFormat(QImage::Format_RGBA8888) : image.convertToFormat(QImage::Format_RGB888); _hints[_imageKey] = MakeGlibVariant(std::tuple{ convertedImage.width(), convertedImage.height(), int(convertedImage.bytesPerLine()), convertedImage.hasAlphaChannel(), 8, convertedImage.hasAlphaChannel() ? 4 : 3, std::vector( convertedImage.constBits(), convertedImage.constBits() + convertedImage.sizeInBytes()), }); } void NotificationData::notificationClosed(uint id, uint reason) { /* * 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); } } void NotificationData::actionInvoked( uint id, const Glib::ustring &actionName) { if (id != _notificationId) { return; } if (actionName == "default" || actionName == "mail-reply-sender") { _manager->notificationActivated(_id); } else if (actionName == "mail-mark-read") { _manager->notificationReplied(_id, {}); } } void NotificationData::activationToken(uint id, const Glib::ustring &token) { if (id == _notificationId) { qputenv("XDG_ACTIVATION_TOKEN", QByteArray::fromStdString(token)); } } void NotificationData::notificationReplied( uint id, const Glib::ustring &text) { if (id == _notificationId) { _manager->notificationReplied( _id, { QString::fromStdString(text), {} }); } } } // namespace bool SkipAudioForCustom() { return false; } bool SkipToastForCustom() { return false; } bool SkipFlashBounceForCustom() { return false; } bool Supported() { return ServiceRegistered || Gio::Application::get_default(); } bool Enforced() { // Wayland doesn't support positioning // and custom notifications don't work here return IsWayland() || OptionGApplication.value(); } bool ByDefault() { // A list of capabilities that offer feature parity // with custom notifications static const auto NeededCapabilities = { // To show message content u"body"_q, // To make the sender name bold u"body-markup"_q, // To have buttons on notifications u"actions"_q, // To have quick reply u"inline-reply"_q, // To not to play sound with Don't Disturb activated // (no, using sound capability is not a way) u"inhibitions"_q, }; return ranges::all_of(NeededCapabilities, [&](const auto &capability) { return CurrentCapabilities.contains(capability); }); } void Create(Window::Notifications::System *system) { static auto ServiceWatcher = CreateServiceWatcher(); const auto managerSetter = [=] { using ManagerType = Window::Notifications::ManagerType; if ((Core::App().settings().nativeNotifications() || Enforced()) && Supported()) { if (system->manager().type() != ManagerType::Native) { system->setManager(std::make_unique(system)); } } else if (Enforced()) { if (system->manager().type() != ManagerType::Dummy) { using DummyManager = Window::Notifications::DummyManager; system->setManager(std::make_unique(system)); } } else if (system->manager().type() != ManagerType::Default) { system->setManager(nullptr); } }; if (Gio::Application::get_default()) { ServiceWatcher = nullptr; ServiceRegistered = false; CurrentServerInformation = std::nullopt; CurrentCapabilities = QStringList{}; managerSetter(); return; } const auto counter = std::make_shared(2); const auto oneReady = [=] { if (!--*counter) { // GApplication may be created while the reply is received if (Gio::Application::get_default()) { Core::App().notifications().createManager(); return; } managerSetter(); } }; // snap doesn't allow access when the daemon is not running :( StartServiceAsync([=] { // GApplication may be created while the reply is received if (Gio::Application::get_default()) { Core::App().notifications().createManager(); return; } ServiceRegistered = GetServiceRegistered(); if (!ServiceRegistered) { CurrentServerInformation = std::nullopt; CurrentCapabilities = QStringList{}; managerSetter(); return; } GetServerInformation([=](const std::optional &result) { CurrentServerInformation = result; oneReady(); }); GetCapabilities([=](const QStringList &result) { CurrentCapabilities = result; oneReady(); }); }); } class Manager::Private : public base::has_weak_ptr { public: explicit Private(not_null manager); void showNotification( not_null peer, MsgId topicRootId, Ui::PeerUserpicView &userpicView, MsgId msgId, const QString &title, const QString &subtitle, const QString &msg, DisplayOptions options); void clearAll(); void clearFromItem(not_null item); void clearFromTopic(not_null topic); void clearFromHistory(not_null history); void clearFromSession(not_null session); void clearNotification(NotificationId id); [[nodiscard]] bool inhibited() const { return _inhibited; } ~Private(); private: const not_null _manager; base::flat_map< ContextId, base::flat_map> _notifications; Glib::RefPtr _dbusConnection; bool _inhibited = false; uint _inhibitedSignalId = 0; }; Manager::Private::Private(not_null manager) : _manager(manager) { const auto serverInformation = CurrentServerInformation; const auto capabilities = CurrentCapabilities; if (serverInformation.has_value()) { LOG(("Notification daemon product name: %1") .arg(serverInformation->name)); LOG(("Notification daemon vendor name: %1") .arg(serverInformation->vendor)); LOG(("Notification daemon version: %1") .arg(serverInformation->version.toString())); LOG(("Notification daemon specification version: %1") .arg(serverInformation->specVersion.toString())); } if (!capabilities.isEmpty()) { LOG(("Notification daemon capabilities: %1") .arg(capabilities.join(", "))); } if (capabilities.contains(u"inhibitions"_q)) { Noexcept([&] { _dbusConnection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); }); if (!_dbusConnection) { return; } const auto weak = base::make_weak(this); GetInhibited(crl::guard(weak, [=](bool result) { _inhibited = result; })); _inhibitedSignalId = _dbusConnection->signal_subscribe( [=]( const Glib::RefPtr &connection, const Glib::ustring &sender_name, const Glib::ustring &object_path, const Glib::ustring &interface_name, const Glib::ustring &signal_name, Glib::VariantContainerBase parameters) { Noexcept([&] { const auto interface = GlibVariantCast( parameters.get_child(0)); if (interface != kInterface.data()) { return; } const auto inhibited = GlibVariantCast( GlibVariantCast< std::map >(parameters.get_child(1)).at("Inhibited")); crl::on_main(weak, [=] { _inhibited = inhibited; }); }); }, std::string(kService), std::string(kPropertiesInterface), "PropertiesChanged", std::string(kObjectPath)); } } void Manager::Private::showNotification( not_null peer, MsgId topicRootId, Ui::PeerUserpicView &userpicView, MsgId msgId, const QString &title, const QString &subtitle, const QString &msg, DisplayOptions options) { const auto key = ContextId{ .sessionId = peer->session().uniqueId(), .peerId = peer->id, .topicRootId = topicRootId, }; const auto notificationId = NotificationId{ .contextId = key, .msgId = msgId, }; auto notification = std::make_unique( _manager, notificationId); const auto inited = notification->init( title, subtitle, msg, options); if (!inited) { return; } if (!options.hideNameAndPhoto) { notification->setImage( Window::Notifications::GenerateUserpic(peer, userpicView)); } auto i = _notifications.find(key); if (i != end(_notifications)) { auto j = i->second.find(msgId); if (j != end(i->second)) { auto oldNotification = std::move(j->second); i->second.erase(j); oldNotification->close(); i = _notifications.find(key); } } if (i == end(_notifications)) { i = _notifications.emplace( key, base::flat_map()).first; } const auto j = i->second.emplace( msgId, std::move(notification)).first; j->second->show(); } void Manager::Private::clearAll() { for (const auto &[key, notifications] : base::take(_notifications)) { for (const auto &[msgId, notification] : notifications) { notification->close(); } } } void Manager::Private::clearFromItem(not_null item) { const auto key = ContextId{ .sessionId = item->history()->session().uniqueId(), .peerId = item->history()->peer->id, .topicRootId = item->topicRootId(), }; const auto i = _notifications.find(key); if (i == _notifications.cend()) { return; } const auto j = i->second.find(item->id); if (j == i->second.end()) { return; } const auto taken = base::take(j->second); i->second.erase(j); if (i->second.empty()) { _notifications.erase(i); } taken->close(); } void Manager::Private::clearFromTopic(not_null topic) { const auto key = ContextId{ .sessionId = topic->session().uniqueId(), .peerId = topic->history()->peer->id }; const auto i = _notifications.find(key); if (i != _notifications.cend()) { const auto temp = base::take(i->second); _notifications.erase(i); for (const auto &[msgId, notification] : temp) { notification->close(); } } } void Manager::Private::clearFromHistory(not_null history) { const auto sessionId = history->session().uniqueId(); const auto peerId = history->peer->id; auto i = _notifications.lower_bound(ContextId{ .sessionId = sessionId, .peerId = peerId, }); while (i != _notifications.cend() && i->first.sessionId == sessionId && i->first.peerId == peerId) { const auto temp = base::take(i->second); i = _notifications.erase(i); for (const auto &[msgId, notification] : temp) { notification->close(); } } } void Manager::Private::clearFromSession(not_null session) { const auto sessionId = session->uniqueId(); auto i = _notifications.lower_bound(ContextId{ .sessionId = sessionId, }); while (i != _notifications.cend() && i->first.sessionId == sessionId) { const auto temp = base::take(i->second); i = _notifications.erase(i); for (const auto &[msgId, notification] : temp) { notification->close(); } } } void Manager::Private::clearNotification(NotificationId id) { auto i = _notifications.find(id.contextId); if (i != _notifications.cend()) { if (i->second.remove(id.msgId) && i->second.empty()) { _notifications.erase(i); } } } Manager::Private::~Private() { clearAll(); if (_dbusConnection) { if (_inhibitedSignalId != 0) { _dbusConnection->signal_unsubscribe(_inhibitedSignalId); } } } Manager::Manager(not_null system) : NativeManager(system) , _private(std::make_unique(this)) { } void Manager::clearNotification(NotificationId id) { _private->clearNotification(id); } Manager::~Manager() = default; void Manager::doShowNativeNotification( not_null peer, MsgId topicRootId, Ui::PeerUserpicView &userpicView, MsgId msgId, const QString &title, const QString &subtitle, const QString &msg, DisplayOptions options) { _private->showNotification( peer, topicRootId, userpicView, msgId, title, subtitle, msg, options); } void Manager::doClearAllFast() { _private->clearAll(); } void Manager::doClearFromItem(not_null item) { _private->clearFromItem(item); } void Manager::doClearFromTopic(not_null topic) { _private->clearFromTopic(topic); } void Manager::doClearFromHistory(not_null history) { _private->clearFromHistory(history); } void Manager::doClearFromSession(not_null session) { _private->clearFromSession(session); } bool Manager::doSkipAudio() const { return _private->inhibited(); } bool Manager::doSkipToast() const { return false; } bool Manager::doSkipFlashBounce() const { return _private->inhibited(); } } // namespace Notifications } // namespace Platform