notifications_manager_linux.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "platform/linux/notifications_manager_linux.h"
  8. #include "base/options.h"
  9. #include "base/platform/base_platform_info.h"
  10. #include "base/platform/linux/base_linux_dbus_utilities.h"
  11. #include "platform/platform_specific.h"
  12. #include "core/application.h"
  13. #include "core/sandbox.h"
  14. #include "core/core_settings.h"
  15. #include "data/data_forum_topic.h"
  16. #include "history/history.h"
  17. #include "history/history_item.h"
  18. #include "main/main_session.h"
  19. #include "media/audio/media_audio_local_cache.h"
  20. #include "lang/lang_keys.h"
  21. #include "base/weak_ptr.h"
  22. #include "window/notifications_utilities.h"
  23. #include <QtCore/QBuffer>
  24. #include <QtCore/QVersionNumber>
  25. #include <QtGui/QGuiApplication>
  26. #include <ksandbox.h>
  27. #include <xdgnotifications/xdgnotifications.hpp>
  28. #include <dlfcn.h>
  29. namespace Platform {
  30. namespace Notifications {
  31. namespace {
  32. using namespace gi::repository;
  33. namespace GObject = gi::repository::GObject;
  34. constexpr auto kService = "org.freedesktop.Notifications";
  35. constexpr auto kObjectPath = "/org/freedesktop/Notifications";
  36. struct ServerInformation {
  37. std::string name;
  38. std::string vendor;
  39. QVersionNumber version;
  40. QVersionNumber specVersion;
  41. };
  42. bool ServiceRegistered = false;
  43. ServerInformation CurrentServerInformation;
  44. std::vector<std::string> CurrentCapabilities;
  45. [[nodiscard]] bool HasCapability(const char *value) {
  46. return ranges::contains(CurrentCapabilities, value);
  47. }
  48. std::unique_ptr<base::Platform::DBus::ServiceWatcher> CreateServiceWatcher() {
  49. auto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr);
  50. if (!connection) {
  51. return nullptr;
  52. }
  53. const auto activatable = [&] {
  54. const auto names = base::Platform::DBus::ListActivatableNames(
  55. connection.gobj_());
  56. if (!names) {
  57. // avoid service restart loop in sandboxed environments
  58. return true;
  59. }
  60. return ranges::contains(*names, kService);
  61. }();
  62. return std::make_unique<base::Platform::DBus::ServiceWatcher>(
  63. connection.gobj_(),
  64. kService,
  65. [=](
  66. const std::string &service,
  67. const std::string &oldOwner,
  68. const std::string &newOwner) {
  69. Core::Sandbox::Instance().customEnterFromEventLoop([&] {
  70. if (activatable && newOwner.empty()) {
  71. Core::App().notifications().clearAll();
  72. } else {
  73. Core::App().notifications().createManager();
  74. }
  75. });
  76. });
  77. }
  78. void StartServiceAsync(Gio::DBusConnection connection, Fn<void()> callback) {
  79. namespace DBus = base::Platform::DBus;
  80. DBus::StartServiceByNameAsync(
  81. connection.gobj_(),
  82. kService,
  83. [=](Fn<DBus::Result<DBus::StartReply>()> result) {
  84. Core::Sandbox::Instance().customEnterFromEventLoop([&] {
  85. // get the error if any
  86. if (const auto ret = result(); !ret) {
  87. const auto &error = *static_cast<GLib::Error*>(
  88. ret.error().get());
  89. if (error.gobj_()->domain != G_DBUS_ERROR
  90. || error.code_()
  91. != G_DBUS_ERROR_SERVICE_UNKNOWN) {
  92. Gio::DBusErrorNS_::strip_remote_error(error);
  93. LOG(("Native Notification Error: %1").arg(
  94. error.message_().c_str()));
  95. }
  96. }
  97. callback();
  98. });
  99. });
  100. }
  101. std::string GetImageKey() {
  102. const auto &specVersion = CurrentServerInformation.specVersion;
  103. if (specVersion >= QVersionNumber(1, 2)) {
  104. return "image-data";
  105. } else if (specVersion == QVersionNumber(1, 1)) {
  106. return "image_data";
  107. }
  108. return "icon_data";
  109. }
  110. bool UseGNotification() {
  111. if (!Gio::Application::get_default()) {
  112. return false;
  113. }
  114. if (Window::Notifications::OptionGNotification.value()) {
  115. return true;
  116. }
  117. return KSandbox::isFlatpak() && !ServiceRegistered;
  118. }
  119. } // namespace
  120. class Manager::Private : public base::has_weak_ptr {
  121. public:
  122. explicit Private(not_null<Manager*> manager);
  123. void init(XdgNotifications::NotificationsProxy proxy);
  124. void showNotification(
  125. NotificationInfo &&info,
  126. Ui::PeerUserpicView &userpicView);
  127. void clearAll();
  128. void clearFromItem(not_null<HistoryItem*> item);
  129. void clearFromTopic(not_null<Data::ForumTopic*> topic);
  130. void clearFromHistory(not_null<History*> history);
  131. void clearFromSession(not_null<Main::Session*> session);
  132. void clearNotification(NotificationId id);
  133. void invokeIfNotInhibited(Fn<void()> callback);
  134. private:
  135. struct NotificationData : public base::has_weak_ptr {
  136. std::variant<v::null_t, uint, std::string> id;
  137. rpl::lifetime lifetime;
  138. };
  139. using Notification = std::unique_ptr<NotificationData>;
  140. const not_null<Manager*> _manager;
  141. Gio::Application _application;
  142. XdgNotifications::NotificationsProxy _proxy;
  143. XdgNotifications::Notifications _interface;
  144. Media::Audio::LocalDiskCache _sounds;
  145. base::flat_map<
  146. ContextId,
  147. base::flat_map<MsgId, Notification>> _notifications;
  148. rpl::lifetime _lifetime;
  149. };
  150. bool SkipToastForCustom() {
  151. return false;
  152. }
  153. void MaybePlaySoundForCustom(Fn<void()> playSound) {
  154. playSound();
  155. }
  156. void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
  157. flashBounce();
  158. }
  159. bool WaitForInputForCustom() {
  160. return true;
  161. }
  162. bool Supported() {
  163. return ServiceRegistered || UseGNotification();
  164. }
  165. bool Enforced() {
  166. // Wayland doesn't support positioning
  167. // and custom notifications don't work here
  168. return IsWayland()
  169. || (Gio::Application::get_default()
  170. && Window::Notifications::OptionGNotification.value());
  171. }
  172. bool ByDefault() {
  173. // The capabilities are static, equivalent to 'body' and 'actions' only
  174. if (UseGNotification()) {
  175. return false;
  176. }
  177. // A list of capabilities that offer feature parity
  178. // with custom notifications
  179. return ranges::all_of(std::array{
  180. // To show message content
  181. "body",
  182. // To have buttons on notifications
  183. "actions",
  184. // To have quick reply
  185. "inline-reply",
  186. }, HasCapability) && ranges::any_of(std::array{
  187. // To not to play sound with Don't Disturb activated
  188. "sound",
  189. "inhibitions",
  190. }, HasCapability);
  191. }
  192. void Create(Window::Notifications::System *system) {
  193. static const auto ServiceWatcher = CreateServiceWatcher();
  194. const auto managerSetter = [=](
  195. XdgNotifications::NotificationsProxy proxy) {
  196. system->setManager([=] {
  197. auto manager = std::make_unique<Manager>(system);
  198. manager->_private->init(proxy);
  199. return manager;
  200. });
  201. };
  202. const auto counter = std::make_shared<int>(2);
  203. const auto oneReady = [=](XdgNotifications::NotificationsProxy proxy) {
  204. if (!--*counter) {
  205. managerSetter(proxy);
  206. }
  207. };
  208. XdgNotifications::NotificationsProxy::new_for_bus(
  209. Gio::BusType::SESSION_,
  210. Gio::DBusProxyFlags::NONE_,
  211. kService,
  212. kObjectPath,
  213. [=](GObject::Object, Gio::AsyncResult res) {
  214. auto proxy =
  215. XdgNotifications::NotificationsProxy::new_for_bus_finish(
  216. res,
  217. nullptr);
  218. if (!proxy) {
  219. ServiceRegistered = false;
  220. CurrentServerInformation = {};
  221. CurrentCapabilities = {};
  222. managerSetter(nullptr);
  223. return;
  224. }
  225. ServiceRegistered = bool(proxy.get_name_owner());
  226. if (!ServiceRegistered) {
  227. CurrentServerInformation = {};
  228. CurrentCapabilities = {};
  229. managerSetter(proxy);
  230. return;
  231. }
  232. auto interface = XdgNotifications::Notifications(proxy);
  233. interface.call_get_server_information([=](
  234. GObject::Object,
  235. Gio::AsyncResult res) mutable {
  236. const auto result =
  237. interface.call_get_server_information_finish(res);
  238. if (result) {
  239. CurrentServerInformation = {
  240. std::get<1>(*result),
  241. std::get<2>(*result),
  242. QVersionNumber::fromString(
  243. QString::fromStdString(std::get<3>(*result))
  244. ).normalized(),
  245. QVersionNumber::fromString(
  246. QString::fromStdString(std::get<4>(*result))
  247. ).normalized(),
  248. };
  249. } else {
  250. Gio::DBusErrorNS_::strip_remote_error(result.error());
  251. LOG(("Native Notification Error: %1").arg(
  252. result.error().message_().c_str()));
  253. CurrentServerInformation = {};
  254. }
  255. oneReady(proxy);
  256. });
  257. interface.call_get_capabilities([=](
  258. GObject::Object,
  259. Gio::AsyncResult res) mutable {
  260. const auto result = interface.call_get_capabilities_finish(
  261. res);
  262. if (result) {
  263. CurrentCapabilities = std::get<1>(*result)
  264. | ranges::to<std::vector<std::string>>;
  265. } else {
  266. Gio::DBusErrorNS_::strip_remote_error(result.error());
  267. LOG(("Native Notification Error: %1").arg(
  268. result.error().message_().c_str()));
  269. CurrentCapabilities = {};
  270. }
  271. oneReady(proxy);
  272. });
  273. });
  274. }
  275. Manager::Private::Private(not_null<Manager*> manager)
  276. : _manager(manager)
  277. , _application(UseGNotification()
  278. ? Gio::Application::get_default()
  279. : nullptr)
  280. , _sounds(cWorkingDir() + u"tdata/audio_cache"_q) {
  281. const auto &serverInformation = CurrentServerInformation;
  282. if (!serverInformation.name.empty()) {
  283. LOG(("Notification daemon product name: %1")
  284. .arg(serverInformation.name.c_str()));
  285. }
  286. if (!serverInformation.vendor.empty()) {
  287. LOG(("Notification daemon vendor name: %1")
  288. .arg(serverInformation.vendor.c_str()));
  289. }
  290. if (!serverInformation.version.isNull()) {
  291. LOG(("Notification daemon version: %1")
  292. .arg(serverInformation.version.toString()));
  293. }
  294. if (!serverInformation.specVersion.isNull()) {
  295. LOG(("Notification daemon specification version: %1")
  296. .arg(serverInformation.specVersion.toString()));
  297. }
  298. if (!CurrentCapabilities.empty()) {
  299. LOG(("Notification daemon capabilities: %1").arg(
  300. ranges::fold_left(
  301. CurrentCapabilities,
  302. "",
  303. [](const std::string &a, const std::string &b) {
  304. return a + (a.empty() ? "" : ", ") + b;
  305. }).c_str()));
  306. }
  307. if (auto actionMap = Gio::ActionMap(_application)) {
  308. const auto dictToNotificationId = [](GLib::VariantDict dict) {
  309. return NotificationId{
  310. .contextId = ContextId{
  311. .sessionId = dict.lookup_value("session").get_uint64(),
  312. .peerId = PeerId(dict.lookup_value("peer").get_uint64()),
  313. .topicRootId = dict.lookup_value("topic").get_int64(),
  314. },
  315. .msgId = dict.lookup_value("msgid").get_int64(),
  316. };
  317. };
  318. auto activate = gi::wrap(
  319. G_SIMPLE_ACTION(
  320. actionMap.lookup_action("notification-activate").gobj_()),
  321. gi::transfer_none);
  322. const auto activateSig = activate.signal_activate().connect([=](
  323. Gio::SimpleAction,
  324. GLib::Variant parameter) {
  325. Core::Sandbox::Instance().customEnterFromEventLoop([&] {
  326. _manager->notificationActivated(
  327. dictToNotificationId(GLib::VariantDict::new_(parameter)));
  328. });
  329. });
  330. _lifetime.add([=]() mutable {
  331. activate.disconnect(activateSig);
  332. });
  333. auto markAsRead = gi::wrap(
  334. G_SIMPLE_ACTION(
  335. actionMap.lookup_action("notification-mark-as-read").gobj_()),
  336. gi::transfer_none);
  337. const auto markAsReadSig = markAsRead.signal_activate().connect([=](
  338. Gio::SimpleAction,
  339. GLib::Variant parameter) {
  340. Core::Sandbox::Instance().customEnterFromEventLoop([&] {
  341. _manager->notificationReplied(
  342. dictToNotificationId(GLib::VariantDict::new_(parameter)),
  343. {});
  344. });
  345. });
  346. _lifetime.add([=]() mutable {
  347. markAsRead.disconnect(markAsReadSig);
  348. });
  349. }
  350. }
  351. void Manager::Private::init(XdgNotifications::NotificationsProxy proxy) {
  352. _proxy = proxy;
  353. _interface = proxy;
  354. if (_application || !_interface) {
  355. return;
  356. }
  357. const auto actionInvoked = _interface.signal_action_invoked().connect([=](
  358. XdgNotifications::Notifications,
  359. uint id,
  360. std::string actionName) {
  361. Core::Sandbox::Instance().customEnterFromEventLoop([&] {
  362. for (const auto &[key, notifications] : _notifications) {
  363. for (const auto &[msgId, notification] : notifications) {
  364. const auto &nid = notification->id;
  365. if (v::is<uint>(nid) && v::get<uint>(nid) == id) {
  366. if (actionName == "default") {
  367. _manager->notificationActivated({ key, msgId });
  368. } else if (actionName == "mail-mark-read") {
  369. _manager->notificationReplied({ key, msgId }, {});
  370. }
  371. return;
  372. }
  373. }
  374. }
  375. });
  376. });
  377. _lifetime.add([=] {
  378. _interface.disconnect(actionInvoked);
  379. });
  380. const auto replied = _interface.signal_notification_replied().connect([=](
  381. XdgNotifications::Notifications,
  382. uint id,
  383. std::string text) {
  384. Core::Sandbox::Instance().customEnterFromEventLoop([&] {
  385. for (const auto &[key, notifications] : _notifications) {
  386. for (const auto &[msgId, notification] : notifications) {
  387. const auto &nid = notification->id;
  388. if (v::is<uint>(nid) && v::get<uint>(nid) == id) {
  389. _manager->notificationReplied(
  390. { key, msgId },
  391. { QString::fromStdString(text), {} });
  392. return;
  393. }
  394. }
  395. }
  396. });
  397. });
  398. _lifetime.add([=] {
  399. _interface.disconnect(replied);
  400. });
  401. const auto tokenSignal = _interface.signal_activation_token().connect([=](
  402. XdgNotifications::Notifications,
  403. uint id,
  404. std::string token) {
  405. for (const auto &[key, notifications] : _notifications) {
  406. for (const auto &[msgId, notification] : notifications) {
  407. const auto &nid = notification->id;
  408. if (v::is<uint>(nid) && v::get<uint>(nid) == id) {
  409. GLib::setenv("XDG_ACTIVATION_TOKEN", token, true);
  410. return;
  411. }
  412. }
  413. }
  414. });
  415. _lifetime.add([=] {
  416. _interface.disconnect(tokenSignal);
  417. });
  418. const auto closed = _interface.signal_notification_closed().connect([=](
  419. XdgNotifications::Notifications,
  420. uint id,
  421. uint reason) {
  422. Core::Sandbox::Instance().customEnterFromEventLoop([&] {
  423. for (const auto &[key, notifications] : _notifications) {
  424. for (const auto &[msgId, notification] : notifications) {
  425. /*
  426. * From: https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html
  427. * The reason the notification was closed
  428. * 1 - The notification expired.
  429. * 2 - The notification was dismissed by the user.
  430. * 3 - The notification was closed by a call to CloseNotification.
  431. * 4 - Undefined/reserved reasons.
  432. *
  433. * If the notification was dismissed by the user (reason == 2), the notification is not kept in notification history.
  434. * We do not need to send a "CloseNotification" call later to clear it from history.
  435. * Therefore we can drop the notification reference now.
  436. * In all other cases we keep the notification reference so that we may clear the notification later from history,
  437. * if the message for that notification is read (e.g. chat is opened or read from another device).
  438. */
  439. const auto &nid = notification->id;
  440. if (v::is<uint>(nid) && v::get<uint>(nid) == id && reason == 2) {
  441. clearNotification({ key, msgId });
  442. return;
  443. }
  444. }
  445. }
  446. });
  447. });
  448. _lifetime.add([=] {
  449. _interface.disconnect(closed);
  450. });
  451. }
  452. void Manager::Private::showNotification(
  453. NotificationInfo &&info,
  454. Ui::PeerUserpicView &userpicView) {
  455. const auto peer = info.peer;
  456. const auto options = info.options;
  457. const auto key = ContextId{
  458. .sessionId = peer->session().uniqueId(),
  459. .peerId = peer->id,
  460. .topicRootId = info.topicRootId,
  461. };
  462. const auto notificationId = NotificationId{
  463. .contextId = key,
  464. .msgId = info.itemId,
  465. };
  466. auto notification = _application
  467. ? Gio::Notification::new_(
  468. info.subtitle.isEmpty()
  469. ? info.title.toStdString()
  470. : info.subtitle.toStdString()
  471. + " (" + info.title.toStdString() + ')')
  472. : Gio::Notification();
  473. std::vector<gi::cstring> actions;
  474. auto hints = GLib::VariantDict::new_();
  475. if (notification) {
  476. notification.set_body(info.message.toStdString());
  477. notification.set_icon(
  478. Gio::ThemedIcon::new_(ApplicationIconName().toStdString()));
  479. // for chat messages, according to
  480. // https://docs.gtk.org/gio/enum.NotificationPriority.html
  481. notification.set_priority(Gio::NotificationPriority::HIGH_);
  482. // glib 2.70+, we keep glib 2.56+ compatibility
  483. static const auto set_category = [] {
  484. // reset dlerror after dlsym call
  485. const auto guard = gsl::finally([] { dlerror(); });
  486. return reinterpret_cast<void(*)(GNotification*, const gchar*)>(
  487. dlsym(RTLD_DEFAULT, "g_notification_set_category"));
  488. }();
  489. if (set_category) {
  490. set_category(notification.gobj_(), "im.received");
  491. }
  492. const auto notificationVariant = GLib::Variant::new_array({
  493. GLib::Variant::new_dict_entry(
  494. GLib::Variant::new_string("session"),
  495. GLib::Variant::new_variant(
  496. GLib::Variant::new_uint64(peer->session().uniqueId()))),
  497. GLib::Variant::new_dict_entry(
  498. GLib::Variant::new_string("peer"),
  499. GLib::Variant::new_variant(
  500. GLib::Variant::new_uint64(peer->id.value))),
  501. GLib::Variant::new_dict_entry(
  502. GLib::Variant::new_string("peer"),
  503. GLib::Variant::new_variant(
  504. GLib::Variant::new_uint64(peer->id.value))),
  505. GLib::Variant::new_dict_entry(
  506. GLib::Variant::new_string("topic"),
  507. GLib::Variant::new_variant(
  508. GLib::Variant::new_int64(info.topicRootId.bare))),
  509. GLib::Variant::new_dict_entry(
  510. GLib::Variant::new_string("msgid"),
  511. GLib::Variant::new_variant(
  512. GLib::Variant::new_int64(info.itemId.bare))),
  513. });
  514. notification.set_default_action_and_target(
  515. "app.notification-activate",
  516. notificationVariant);
  517. if (!options.hideMarkAsRead) {
  518. notification.add_button_with_target(
  519. tr::lng_context_mark_read(tr::now).toStdString(),
  520. "app.notification-mark-as-read",
  521. notificationVariant);
  522. }
  523. } else {
  524. if (HasCapability("actions")) {
  525. actions.push_back("default");
  526. actions.push_back(tr::lng_open_link(tr::now).toStdString());
  527. if (!options.hideMarkAsRead) {
  528. // icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
  529. actions.push_back("mail-mark-read");
  530. actions.push_back(
  531. tr::lng_context_mark_read(tr::now).toStdString());
  532. }
  533. if (HasCapability("inline-reply")
  534. && !options.hideReplyButton) {
  535. actions.push_back("inline-reply");
  536. actions.push_back(
  537. tr::lng_notification_reply(tr::now).toStdString());
  538. }
  539. actions.push_back({});
  540. }
  541. if (HasCapability("action-icons")) {
  542. hints.insert_value(
  543. "action-icons",
  544. GLib::Variant::new_boolean(true));
  545. }
  546. if (HasCapability("sound")) {
  547. const auto sound = info.sound
  548. ? info.sound()
  549. : Media::Audio::LocalSound();
  550. const auto path = sound
  551. ? _sounds.path(sound).toStdString()
  552. : std::string();
  553. if (!path.empty()) {
  554. hints.insert_value(
  555. "sound-file",
  556. GLib::Variant::new_string(path));
  557. } else {
  558. hints.insert_value(
  559. "suppress-sound",
  560. GLib::Variant::new_boolean(true));
  561. }
  562. }
  563. if (HasCapability("x-canonical-append")) {
  564. hints.insert_value(
  565. "x-canonical-append",
  566. GLib::Variant::new_string("true"));
  567. }
  568. hints.insert_value(
  569. "category",
  570. GLib::Variant::new_string("im.received"));
  571. hints.insert_value("desktop-entry", GLib::Variant::new_string(
  572. QGuiApplication::desktopFileName().toStdString()));
  573. }
  574. const auto imageKey = GetImageKey();
  575. if (!options.hideNameAndPhoto) {
  576. if (notification) {
  577. QByteArray imageData;
  578. QBuffer buffer(&imageData);
  579. buffer.open(QIODevice::WriteOnly);
  580. Window::Notifications::GenerateUserpic(peer, userpicView).save(
  581. &buffer,
  582. "PNG");
  583. notification.set_icon(
  584. Gio::BytesIcon::new_(
  585. GLib::Bytes::new_with_free_func(
  586. reinterpret_cast<const uchar*>(imageData.constData()),
  587. imageData.size(),
  588. [imageData] {})));
  589. } else if (!imageKey.empty()) {
  590. const auto image = Window::Notifications::GenerateUserpic(
  591. peer,
  592. userpicView
  593. ).convertToFormat(QImage::Format_RGBA8888);
  594. hints.insert_value(imageKey, GLib::Variant::new_tuple({
  595. GLib::Variant::new_int32(image.width()),
  596. GLib::Variant::new_int32(image.height()),
  597. GLib::Variant::new_int32(image.bytesPerLine()),
  598. GLib::Variant::new_boolean(true),
  599. GLib::Variant::new_int32(8),
  600. GLib::Variant::new_int32(4),
  601. GLib::Variant::new_from_data(
  602. GLib::VariantType::new_("ay"),
  603. reinterpret_cast<const uchar*>(image.constBits()),
  604. image.sizeInBytes(),
  605. true,
  606. [image] {}),
  607. }));
  608. }
  609. }
  610. const auto &data
  611. = _notifications[key][info.itemId]
  612. = std::make_unique<NotificationData>();
  613. data->lifetime.add([=, notification = data.get()] {
  614. v::match(notification->id, [&](const std::string &id) {
  615. _application.withdraw_notification(id);
  616. }, [&](uint id) {
  617. _interface.call_close_notification(id, nullptr);
  618. }, [](v::null_t) {});
  619. });
  620. if (notification) {
  621. const auto id = Gio::dbus_generate_guid();
  622. data->id = id;
  623. _application.send_notification(id, notification);
  624. } else {
  625. // work around snap's activation restriction
  626. const auto weak = base::make_weak(data);
  627. StartServiceAsync(
  628. _proxy.get_connection(),
  629. crl::guard(weak, [=]() mutable {
  630. const auto hasImage = !imageKey.empty()
  631. && hints.lookup_value(imageKey);
  632. const auto hasBodyMarkup = HasCapability("body-markup");
  633. const auto callbackWrap = gi::unwrap(
  634. Gio::AsyncReadyCallback(
  635. crl::guard(this, [=](
  636. GObject::Object,
  637. Gio::AsyncResult res) {
  638. auto &sandbox = Core::Sandbox::Instance();
  639. sandbox.customEnterFromEventLoop([&] {
  640. const auto result
  641. = _interface.call_notify_finish(res);
  642. if (!result) {
  643. Gio::DBusErrorNS_::strip_remote_error(
  644. result.error());
  645. LOG(("Native Notification Error: %1").arg(
  646. result.error().message_().c_str()));
  647. clearNotification(notificationId);
  648. return;
  649. }
  650. if (!weak) {
  651. _interface.call_close_notification(
  652. std::get<1>(*result),
  653. nullptr);
  654. return;
  655. }
  656. weak->id = std::get<1>(*result);
  657. });
  658. })),
  659. gi::scope_async);
  660. xdg_notifications_notifications_call_notify(
  661. _interface.gobj_(),
  662. AppName.data(),
  663. 0,
  664. (!hasImage
  665. ? ApplicationIconName().toStdString()
  666. : std::string()).c_str(),
  667. (hasBodyMarkup || info.subtitle.isEmpty()
  668. ? info.title.toStdString()
  669. : info.subtitle.toStdString()
  670. + " (" + info.title.toStdString() + ')').c_str(),
  671. (hasBodyMarkup
  672. ? info.subtitle.isEmpty()
  673. ? info.message.toHtmlEscaped().toStdString()
  674. : u"<b>%1</b>\n%2"_q.arg(
  675. info.subtitle.toHtmlEscaped(),
  676. info.message.toHtmlEscaped()).toStdString()
  677. : info.message.toStdString()).c_str(),
  678. !actions.empty()
  679. ? (actions
  680. | ranges::views::transform(&gi::cstring::c_str)
  681. | ranges::to_vector).data()
  682. : nullptr,
  683. hints.end().gobj_(),
  684. -1,
  685. nullptr,
  686. &callbackWrap->wrapper,
  687. callbackWrap);
  688. }));
  689. }
  690. }
  691. void Manager::Private::clearAll() {
  692. _notifications.clear();
  693. }
  694. void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
  695. const auto i = _notifications.find(ContextId{
  696. .sessionId = item->history()->session().uniqueId(),
  697. .peerId = item->history()->peer->id,
  698. .topicRootId = item->topicRootId(),
  699. });
  700. if (i != _notifications.cend()
  701. && i->second.remove(item->id)
  702. && i->second.empty()) {
  703. _notifications.erase(i);
  704. }
  705. }
  706. void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
  707. _notifications.remove(ContextId{
  708. .sessionId = topic->session().uniqueId(),
  709. .peerId = topic->history()->peer->id,
  710. .topicRootId = topic->rootId(),
  711. });
  712. }
  713. void Manager::Private::clearFromHistory(not_null<History*> history) {
  714. const auto sessionId = history->session().uniqueId();
  715. const auto peerId = history->peer->id;
  716. auto i = _notifications.lower_bound(ContextId{
  717. .sessionId = sessionId,
  718. .peerId = peerId,
  719. });
  720. while (i != _notifications.cend()
  721. && i->first.sessionId == sessionId
  722. && i->first.peerId == peerId) {
  723. i = _notifications.erase(i);
  724. }
  725. }
  726. void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
  727. const auto sessionId = session->uniqueId();
  728. auto i = _notifications.lower_bound(ContextId{
  729. .sessionId = sessionId,
  730. });
  731. while (i != _notifications.cend() && i->first.sessionId == sessionId) {
  732. i = _notifications.erase(i);
  733. }
  734. }
  735. void Manager::Private::clearNotification(NotificationId id) {
  736. auto i = _notifications.find(id.contextId);
  737. if (i != _notifications.cend()
  738. && i->second.remove(id.msgId)
  739. && i->second.empty()) {
  740. _notifications.erase(i);
  741. }
  742. }
  743. void Manager::Private::invokeIfNotInhibited(Fn<void()> callback) {
  744. if (!_interface.get_inhibited()) {
  745. callback();
  746. }
  747. }
  748. Manager::Manager(not_null<Window::Notifications::System*> system)
  749. : NativeManager(system)
  750. , _private(std::make_unique<Private>(this)) {
  751. }
  752. Manager::~Manager() = default;
  753. void Manager::doShowNativeNotification(
  754. NotificationInfo &&info,
  755. Ui::PeerUserpicView &userpicView) {
  756. _private->showNotification(std::move(info), userpicView);
  757. }
  758. void Manager::doClearAllFast() {
  759. _private->clearAll();
  760. }
  761. void Manager::doClearFromItem(not_null<HistoryItem*> item) {
  762. _private->clearFromItem(item);
  763. }
  764. void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {
  765. _private->clearFromTopic(topic);
  766. }
  767. void Manager::doClearFromHistory(not_null<History*> history) {
  768. _private->clearFromHistory(history);
  769. }
  770. void Manager::doClearFromSession(not_null<Main::Session*> session) {
  771. _private->clearFromSession(session);
  772. }
  773. bool Manager::doSkipToast() const {
  774. return false;
  775. }
  776. void Manager::doMaybePlaySound(Fn<void()> playSound) {
  777. if (UseGNotification()
  778. || !HasCapability("sound")
  779. || !Core::App().settings().desktopNotify()) {
  780. _private->invokeIfNotInhibited(std::move(playSound));
  781. }
  782. }
  783. void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
  784. _private->invokeIfNotInhibited(std::move(flashBounce));
  785. }
  786. } // namespace Notifications
  787. } // namespace Platform