notifications_manager_win.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  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/win/notifications_manager_win.h"
  8. #include "window/notifications_utilities.h"
  9. #include "window/window_session_controller.h"
  10. #include "base/platform/win/base_windows_co_task_mem.h"
  11. #include "base/platform/win/base_windows_rpcndr_h.h"
  12. #include "base/platform/win/base_windows_winrt.h"
  13. #include "base/platform/base_platform_info.h"
  14. #include "base/platform/win/wrl/wrl_module_h.h"
  15. #include "base/qthelp_url.h"
  16. #include "platform/win/windows_app_user_model_id.h"
  17. #include "platform/win/windows_toast_activator.h"
  18. #include "platform/win/windows_dlls.h"
  19. #include "platform/win/specific_win.h"
  20. #include "data/data_forum_topic.h"
  21. #include "history/history.h"
  22. #include "history/history_item.h"
  23. #include "core/application.h"
  24. #include "core/core_settings.h"
  25. #include "lang/lang_keys.h"
  26. #include "main/main_session.h"
  27. #include "mainwindow.h"
  28. #include "windows_quiethours_h.h"
  29. #include "styles/style_chat.h"
  30. #include "styles/style_chat_helpers.h"
  31. #include <QtCore/QOperatingSystemVersion>
  32. #include <Shobjidl.h>
  33. #include <shellapi.h>
  34. #include <strsafe.h>
  35. #include <winrt/Windows.Foundation.h>
  36. #include <winrt/Windows.Data.Xml.Dom.h>
  37. #include <winrt/Windows.UI.Notifications.h>
  38. HICON qt_pixmapToWinHICON(const QPixmap &);
  39. using namespace winrt::Windows::UI::Notifications;
  40. using namespace winrt::Windows::Data::Xml::Dom;
  41. using namespace winrt::Windows::Foundation;
  42. using winrt::com_ptr;
  43. namespace Platform {
  44. namespace Notifications {
  45. namespace {
  46. constexpr auto kQuerySettingsEachMs = 1000;
  47. crl::time LastSettingsQueryMs/* = 0*/;
  48. [[nodiscard]] bool ShouldQuerySettings() {
  49. const auto now = crl::now();
  50. if (LastSettingsQueryMs > 0 && now <= LastSettingsQueryMs + kQuerySettingsEachMs) {
  51. return false;
  52. }
  53. LastSettingsQueryMs = now;
  54. return true;
  55. }
  56. [[nodiscard]] std::wstring NotificationTemplate(
  57. QString id,
  58. Window::Notifications::Manager::DisplayOptions options) {
  59. const auto wid = id.replace('&', "&amp;").toStdWString();
  60. const auto fastReply = LR"(
  61. <input id="fastReply" type="text" placeHolderContent=""/>
  62. <action
  63. content="Send"
  64. arguments="action=reply&amp;)" + wid + LR"("
  65. activationType="background"
  66. imageUri=""
  67. hint-inputId="fastReply"/>
  68. )";
  69. const auto markAsRead = LR"(
  70. <action
  71. content=""
  72. arguments="action=mark&amp;)" + wid + LR"("
  73. activationType="background"/>
  74. )";
  75. const auto actions = (options.hideReplyButton ? L"" : fastReply)
  76. + (options.hideMarkAsRead ? L"" : markAsRead);
  77. return LR"(
  78. <toast launch="action=open&amp;)" + wid + LR"(">
  79. <visual>
  80. <binding template="ToastGeneric">
  81. <image placement="appLogoOverride" hint-crop="circle" src=""/>
  82. <text hint-maxLines="1"></text>
  83. <text></text>
  84. <text></text>
  85. </binding>
  86. </visual>
  87. )" + (actions.empty()
  88. ? L""
  89. : (L"<actions>" + actions + L"</actions>")) + LR"(
  90. <audio silent="true"/>
  91. </toast>
  92. )";
  93. }
  94. bool init() {
  95. if (!IsWindows8OrGreater() || !base::WinRT::Supported()) {
  96. return false;
  97. }
  98. {
  99. using namespace Microsoft::WRL;
  100. const auto hr = Module<OutOfProc>::GetModule().RegisterObjects();
  101. if (!SUCCEEDED(hr)) {
  102. LOG(("App Error: Object registration failed."));
  103. }
  104. }
  105. if (!AppUserModelId::ValidateShortcut()) {
  106. LOG(("App Error: Shortcut validation failed."));
  107. return false;
  108. }
  109. PWSTR appUserModelId = {};
  110. if (!SUCCEEDED(GetCurrentProcessExplicitAppUserModelID(&appUserModelId))) {
  111. return false;
  112. }
  113. const auto appUserModelIdGuard = gsl::finally([&] {
  114. CoTaskMemFree(appUserModelId);
  115. });
  116. if (AppUserModelId::Id() != appUserModelId) {
  117. return false;
  118. }
  119. return true;
  120. }
  121. // Throws.
  122. void SetNodeValueString(
  123. const XmlDocument &xml,
  124. const IXmlNode &node,
  125. const std::wstring &text) {
  126. node.AppendChild(xml.CreateTextNode(text).as<IXmlNode>());
  127. }
  128. // Throws.
  129. void SetAudioSilent(const XmlDocument &toastXml) {
  130. const auto nodeList = toastXml.GetElementsByTagName(L"audio");
  131. if (const auto audioNode = nodeList.Item(0)) {
  132. audioNode.as<IXmlElement>().SetAttribute(L"silent", L"true");
  133. } else {
  134. auto audioElement = toastXml.CreateElement(L"audio");
  135. audioElement.SetAttribute(L"silent", L"true");
  136. auto nodeList = toastXml.GetElementsByTagName(L"toast");
  137. nodeList.Item(0).AppendChild(audioElement.as<IXmlNode>());
  138. }
  139. }
  140. // Throws.
  141. void SetImageSrc(const XmlDocument &toastXml, const std::wstring &path) {
  142. const auto nodeList = toastXml.GetElementsByTagName(L"image");
  143. const auto attributes = nodeList.Item(0).Attributes();
  144. return SetNodeValueString(
  145. toastXml,
  146. attributes.GetNamedItem(L"src"),
  147. L"file:///" + path);
  148. }
  149. // Throws.
  150. void SetReplyIconSrc(const XmlDocument &toastXml, const std::wstring &path) {
  151. const auto nodeList = toastXml.GetElementsByTagName(L"action");
  152. const auto length = int(nodeList.Length());
  153. for (auto i = 0; i != length; ++i) {
  154. const auto attributes = nodeList.Item(i).Attributes();
  155. if (const auto uri = attributes.GetNamedItem(L"imageUri")) {
  156. return SetNodeValueString(toastXml, uri, L"file:///" + path);
  157. }
  158. }
  159. }
  160. // Throws.
  161. void SetReplyPlaceholder(
  162. const XmlDocument &toastXml,
  163. const std::wstring &placeholder) {
  164. const auto nodeList = toastXml.GetElementsByTagName(L"input");
  165. const auto attributes = nodeList.Item(0).Attributes();
  166. return SetNodeValueString(
  167. toastXml,
  168. attributes.GetNamedItem(L"placeHolderContent"),
  169. placeholder);
  170. }
  171. // Throws.
  172. void SetAction(const XmlDocument &toastXml, const QString &id) {
  173. auto nodeList = toastXml.GetElementsByTagName(L"toast");
  174. if (const auto toast = nodeList.Item(0).try_as<XmlElement>()) {
  175. toast.SetAttribute(L"launch", L"action=open&" + id.toStdWString());
  176. }
  177. }
  178. // Throws.
  179. void SetMarkAsReadText(
  180. const XmlDocument &toastXml,
  181. const std::wstring &text) {
  182. const auto nodeList = toastXml.GetElementsByTagName(L"action");
  183. const auto length = int(nodeList.Length());
  184. for (auto i = 0; i != length; ++i) {
  185. const auto attributes = nodeList.Item(i).Attributes();
  186. if (!attributes.GetNamedItem(L"imageUri")) {
  187. return SetNodeValueString(
  188. toastXml,
  189. attributes.GetNamedItem(L"content"),
  190. text);
  191. }
  192. }
  193. }
  194. auto Checked = false;
  195. auto InitSucceeded = false;
  196. void Check() {
  197. InitSucceeded = init();
  198. }
  199. bool QuietHoursEnabled = false;
  200. DWORD QuietHoursValue = 0;
  201. [[nodiscard]] bool UseQuietHoursRegistryEntry() {
  202. static const bool result = [] {
  203. const auto version = QOperatingSystemVersion::current();
  204. // At build 17134 (Redstone 4) the "Quiet hours" was replaced
  205. // by "Focus assist" and it looks like it doesn't use registry.
  206. return (version.majorVersion() == 10)
  207. && (version.minorVersion() == 0)
  208. && (version.microVersion() < 17134);
  209. }();
  210. return result;
  211. }
  212. // Thanks https://stackoverflow.com/questions/35600128/get-windows-quiet-hours-from-win32-or-c-sharp-api
  213. void QueryQuietHours() {
  214. if (!UseQuietHoursRegistryEntry()) {
  215. // There are quiet hours in Windows starting from Windows 8.1
  216. // But there were several reports about the notifications being shut
  217. // down according to the registry while no quiet hours were enabled.
  218. // So we try this method only starting with Windows 10.
  219. return;
  220. }
  221. LPCWSTR lpKeyName = L"Software\\Microsoft\\Windows\\CurrentVersion\\Notifications\\Settings";
  222. LPCWSTR lpValueName = L"NOC_GLOBAL_SETTING_TOASTS_ENABLED";
  223. HKEY key;
  224. auto result = RegOpenKeyEx(HKEY_CURRENT_USER, lpKeyName, 0, KEY_READ, &key);
  225. if (result != ERROR_SUCCESS) {
  226. return;
  227. }
  228. DWORD value = 0, type = 0, size = sizeof(value);
  229. result = RegQueryValueEx(key, lpValueName, 0, &type, (LPBYTE)&value, &size);
  230. RegCloseKey(key);
  231. auto quietHoursEnabled = (result == ERROR_SUCCESS) && (value == 0);
  232. if (QuietHoursEnabled != quietHoursEnabled) {
  233. QuietHoursEnabled = quietHoursEnabled;
  234. QuietHoursValue = value;
  235. LOG(("Quiet hours changed, entry value: %1").arg(value));
  236. } else if (QuietHoursValue != value) {
  237. QuietHoursValue = value;
  238. LOG(("Quiet hours value changed, was value: %1, entry value: %2").arg(QuietHoursValue).arg(value));
  239. }
  240. }
  241. bool FocusAssistBlocks = false;
  242. // Thanks https://www.withinrafael.com/2019/09/19/determine-if-your-app-is-in-a-focus-assist-profiles-priority-list/
  243. void QueryFocusAssist() {
  244. const auto quietHoursSettings = base::WinRT::TryCreateInstance<
  245. IQuietHoursSettings
  246. >(CLSID_QuietHoursSettings, CLSCTX_LOCAL_SERVER);
  247. if (!quietHoursSettings) {
  248. return;
  249. }
  250. auto profileId = base::CoTaskMemString();
  251. auto hr = quietHoursSettings->get_UserSelectedProfile(profileId.put());
  252. if (FAILED(hr) || !profileId) {
  253. return;
  254. }
  255. const auto profileName = QString::fromWCharArray(profileId.data());
  256. if (profileName.endsWith(".alarmsonly", Qt::CaseInsensitive)) {
  257. if (!FocusAssistBlocks) {
  258. LOG(("Focus Assist: Alarms Only."));
  259. FocusAssistBlocks = true;
  260. }
  261. return;
  262. } else if (!profileName.endsWith(".priorityonly", Qt::CaseInsensitive)) {
  263. if (!profileName.endsWith(".unrestricted", Qt::CaseInsensitive)) {
  264. LOG(("Focus Assist Warning: Unknown profile '%1'"
  265. ).arg(profileName));
  266. }
  267. if (FocusAssistBlocks) {
  268. LOG(("Focus Assist: Unrestricted."));
  269. FocusAssistBlocks = false;
  270. }
  271. return;
  272. }
  273. const auto appUserModelId = AppUserModelId::Id();
  274. auto blocked = true;
  275. const auto guard = gsl::finally([&] {
  276. if (FocusAssistBlocks != blocked) {
  277. LOG(("Focus Assist: %1, AppUserModelId: %2, Blocks: %3"
  278. ).arg(profileName
  279. ).arg(QString::fromStdWString(appUserModelId)
  280. ).arg(Logs::b(blocked)));
  281. FocusAssistBlocks = blocked;
  282. }
  283. });
  284. com_ptr<IQuietHoursProfile> profile;
  285. hr = quietHoursSettings->GetProfile(profileId.data(), profile.put());
  286. if (FAILED(hr) || !profile) {
  287. return;
  288. }
  289. auto apps = base::CoTaskMemStringArray();
  290. hr = profile->GetAllowedApps(apps.put_size(), apps.put());
  291. if (FAILED(hr) || !apps) {
  292. return;
  293. }
  294. for (const auto &app : apps) {
  295. if (app && app.data() == appUserModelId) {
  296. blocked = false;
  297. break;
  298. }
  299. }
  300. }
  301. QUERY_USER_NOTIFICATION_STATE UserNotificationState
  302. = QUNS_ACCEPTS_NOTIFICATIONS;
  303. void QueryUserNotificationState() {
  304. if (Dlls::SHQueryUserNotificationState != nullptr) {
  305. QUERY_USER_NOTIFICATION_STATE state;
  306. if (SUCCEEDED(Dlls::SHQueryUserNotificationState(&state))) {
  307. UserNotificationState = state;
  308. }
  309. }
  310. }
  311. void QuerySystemNotificationSettings() {
  312. if (!ShouldQuerySettings()) {
  313. return;
  314. }
  315. QueryQuietHours();
  316. QueryFocusAssist();
  317. QueryUserNotificationState();
  318. }
  319. bool SkipSoundForCustom() {
  320. QuerySystemNotificationSettings();
  321. return (UserNotificationState == QUNS_NOT_PRESENT)
  322. || (UserNotificationState == QUNS_PRESENTATION_MODE)
  323. || (FocusAssistBlocks && Core::App().settings().skipToastsInFocus())
  324. || Core::App().screenIsLocked();
  325. }
  326. bool SkipFlashBounceForCustom() {
  327. return SkipToastForCustom();
  328. }
  329. } // namespace
  330. void MaybePlaySoundForCustom(Fn<void()> playSound) {
  331. if (!SkipSoundForCustom()) {
  332. playSound();
  333. }
  334. }
  335. bool SkipToastForCustom() {
  336. QuerySystemNotificationSettings();
  337. return (UserNotificationState == QUNS_PRESENTATION_MODE)
  338. || (UserNotificationState == QUNS_RUNNING_D3D_FULL_SCREEN)
  339. || (FocusAssistBlocks && Core::App().settings().skipToastsInFocus());
  340. }
  341. void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
  342. if (!SkipFlashBounceForCustom()) {
  343. flashBounce();
  344. }
  345. }
  346. bool WaitForInputForCustom() {
  347. QuerySystemNotificationSettings();
  348. return UserNotificationState != QUNS_BUSY;
  349. }
  350. bool Supported() {
  351. if (!Checked) {
  352. Checked = true;
  353. Check();
  354. }
  355. return InitSucceeded;
  356. }
  357. bool Enforced() {
  358. return false;
  359. }
  360. bool ByDefault() {
  361. return false;
  362. }
  363. void Create(Window::Notifications::System *system) {
  364. system->setManager([=] {
  365. auto result = std::make_unique<Manager>(system);
  366. return result->init() ? std::move(result) : nullptr;
  367. });
  368. }
  369. class Manager::Private {
  370. public:
  371. using Info = Window::Notifications::NativeManager::NotificationInfo;
  372. explicit Private(Manager *instance);
  373. bool init();
  374. bool showNotification(Info &&info, Ui::PeerUserpicView &userpicView);
  375. void clearAll();
  376. void clearFromItem(not_null<HistoryItem*> item);
  377. void clearFromTopic(not_null<Data::ForumTopic*> topic);
  378. void clearFromHistory(not_null<History*> history);
  379. void clearFromSession(not_null<Main::Session*> session);
  380. void beforeNotificationActivated(NotificationId id);
  381. void afterNotificationActivated(
  382. NotificationId id,
  383. not_null<Window::SessionController*> window);
  384. void clearNotification(NotificationId id);
  385. void handleActivation(const ToastActivation &activation);
  386. ~Private();
  387. private:
  388. bool showNotificationInTryCatch(
  389. NotificationInfo &&info,
  390. Ui::PeerUserpicView &userpicView);
  391. void tryHide(const ToastNotification &notification);
  392. [[nodiscard]] std::wstring ensureSendButtonIcon();
  393. Window::Notifications::CachedUserpics _cachedUserpics;
  394. std::wstring _sendButtonIconPath;
  395. std::shared_ptr<Manager*> _guarded;
  396. ToastNotifier _notifier = nullptr;
  397. base::flat_map<
  398. ContextId,
  399. base::flat_map<MsgId, ToastNotification>> _notifications;
  400. rpl::lifetime _lifetime;
  401. };
  402. Manager::Private::Private(Manager *instance)
  403. : _guarded(std::make_shared<Manager*>(instance)) {
  404. ToastActivations(
  405. ) | rpl::start_with_next([=](const ToastActivation &activation) {
  406. handleActivation(activation);
  407. }, _lifetime);
  408. }
  409. bool Manager::Private::init() {
  410. return base::WinRT::Try([&] {
  411. _notifier = ToastNotificationManager::CreateToastNotifier(
  412. AppUserModelId::Id());
  413. });
  414. }
  415. Manager::Private::~Private() {
  416. clearAll();
  417. _notifications.clear();
  418. _notifier = nullptr;
  419. }
  420. void Manager::Private::clearAll() {
  421. if (!_notifier) {
  422. return;
  423. }
  424. for (const auto &[key, notifications] : base::take(_notifications)) {
  425. for (const auto &[msgId, notification] : notifications) {
  426. tryHide(notification);
  427. }
  428. }
  429. }
  430. void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
  431. if (!_notifier) {
  432. return;
  433. }
  434. auto i = _notifications.find(ContextId{
  435. .sessionId = item->history()->session().uniqueId(),
  436. .peerId = item->history()->peer->id,
  437. .topicRootId = item->topicRootId(),
  438. });
  439. if (i == _notifications.cend()) {
  440. return;
  441. }
  442. const auto j = i->second.find(item->id);
  443. if (j == end(i->second)) {
  444. return;
  445. }
  446. const auto taken = std::exchange(j->second, nullptr);
  447. i->second.erase(j);
  448. if (i->second.empty()) {
  449. _notifications.erase(i);
  450. }
  451. tryHide(taken);
  452. }
  453. void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
  454. if (!_notifier) {
  455. return;
  456. }
  457. const auto i = _notifications.find(ContextId{
  458. .sessionId = topic->session().uniqueId(),
  459. .peerId = topic->history()->peer->id,
  460. .topicRootId = topic->rootId(),
  461. });
  462. if (i != _notifications.cend()) {
  463. const auto temp = base::take(i->second);
  464. _notifications.erase(i);
  465. for (const auto &[msgId, notification] : temp) {
  466. tryHide(notification);
  467. }
  468. }
  469. }
  470. void Manager::Private::clearFromHistory(not_null<History*> history) {
  471. if (!_notifier) {
  472. return;
  473. }
  474. const auto sessionId = history->session().uniqueId();
  475. const auto peerId = history->peer->id;
  476. auto i = _notifications.lower_bound(ContextId{
  477. .sessionId = sessionId,
  478. .peerId = peerId,
  479. });
  480. while (i != _notifications.cend()
  481. && i->first.sessionId == sessionId
  482. && i->first.peerId == peerId) {
  483. const auto temp = base::take(i->second);
  484. i = _notifications.erase(i);
  485. for (const auto &[msgId, notification] : temp) {
  486. tryHide(notification);
  487. }
  488. }
  489. }
  490. void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
  491. if (!_notifier) {
  492. return;
  493. }
  494. const auto sessionId = session->uniqueId();
  495. auto i = _notifications.lower_bound(ContextId{
  496. .sessionId = sessionId,
  497. });
  498. while (i != _notifications.cend() && i->first.sessionId == sessionId) {
  499. const auto temp = base::take(i->second);
  500. i = _notifications.erase(i);
  501. for (const auto &[msgId, notification] : temp) {
  502. tryHide(notification);
  503. }
  504. }
  505. }
  506. void Manager::Private::beforeNotificationActivated(NotificationId id) {
  507. clearNotification(id);
  508. }
  509. void Manager::Private::afterNotificationActivated(
  510. NotificationId id,
  511. not_null<Window::SessionController*> window) {
  512. SetForegroundWindow(window->widget()->psHwnd());
  513. }
  514. void Manager::Private::clearNotification(NotificationId id) {
  515. auto i = _notifications.find(id.contextId);
  516. if (i != _notifications.cend()) {
  517. i->second.remove(id.msgId);
  518. if (i->second.empty()) {
  519. _notifications.erase(i);
  520. }
  521. }
  522. }
  523. void Manager::Private::handleActivation(const ToastActivation &activation) {
  524. const auto parsed = qthelp::url_parse_params(activation.args);
  525. const auto pid = parsed.value("pid").toULong();
  526. const auto my = GetCurrentProcessId();
  527. if (pid != my) {
  528. DEBUG_LOG(("Toast Info: "
  529. "Got activation \"%1\", my %2, activating %3."
  530. ).arg(activation.args
  531. ).arg(my
  532. ).arg(pid));
  533. const auto processId = pid;
  534. const auto windowId = 0; // Activate some window.
  535. Platform::ActivateOtherProcess(processId, windowId);
  536. return;
  537. }
  538. const auto action = parsed.value("action");
  539. const auto id = NotificationId{
  540. .contextId = ContextId{
  541. .sessionId = parsed.value("session").toULongLong(),
  542. .peerId = PeerId(parsed.value("peer").toULongLong()),
  543. .topicRootId = MsgId(parsed.value("topic").toLongLong())
  544. },
  545. .msgId = MsgId(parsed.value("msg").toLongLong()),
  546. };
  547. if (!id.contextId.sessionId || !id.contextId.peerId || !id.msgId) {
  548. DEBUG_LOG(("Toast Info: Got activation \"%1\", my %1, skipping."
  549. ).arg(activation.args
  550. ).arg(pid));
  551. return;
  552. }
  553. DEBUG_LOG(("Toast Info: Got activation \"%1\", my %1, handling."
  554. ).arg(activation.args
  555. ).arg(pid));
  556. auto text = TextWithTags();
  557. for (const auto &entry : activation.input) {
  558. if (entry.key == "fastReply") {
  559. text.text = entry.value;
  560. }
  561. }
  562. const auto i = _notifications.find(id.contextId);
  563. if (i == _notifications.cend() || !i->second.contains(id.msgId)) {
  564. return;
  565. }
  566. const auto manager = *_guarded;
  567. if (action == "reply") {
  568. manager->notificationReplied(id, text);
  569. } else if (action == "mark") {
  570. manager->notificationReplied(id, TextWithTags());
  571. } else {
  572. manager->notificationActivated(id, text);
  573. }
  574. }
  575. bool Manager::Private::showNotification(
  576. Info &&info,
  577. Ui::PeerUserpicView &userpicView) {
  578. if (!_notifier) {
  579. return false;
  580. }
  581. return base::WinRT::Try([&] {
  582. return showNotificationInTryCatch(std::move(info), userpicView);
  583. }).value_or(false);
  584. }
  585. std::wstring Manager::Private::ensureSendButtonIcon() {
  586. if (_sendButtonIconPath.empty()) {
  587. const auto path = cWorkingDir() + u"tdata/temp/fast_reply.png"_q;
  588. st::historySendIcon.instance(Qt::white, 300).save(path, "PNG");
  589. _sendButtonIconPath = path.toStdWString();
  590. }
  591. return _sendButtonIconPath;
  592. }
  593. bool Manager::Private::showNotificationInTryCatch(
  594. NotificationInfo &&info,
  595. Ui::PeerUserpicView &userpicView) {
  596. const auto withSubtitle = !info.subtitle.isEmpty();
  597. const auto peer = info.peer;
  598. auto toastXml = XmlDocument();
  599. const auto key = ContextId{
  600. .sessionId = peer->session().uniqueId(),
  601. .peerId = peer->id,
  602. .topicRootId = info.topicRootId,
  603. };
  604. const auto notificationId = NotificationId{
  605. .contextId = key,
  606. .msgId = info.itemId,
  607. };
  608. const auto idString = u"pid=%1&session=%2&peer=%3&topic=%4&msg=%5"_q
  609. .arg(GetCurrentProcessId())
  610. .arg(key.sessionId)
  611. .arg(key.peerId.value)
  612. .arg(info.topicRootId.bare)
  613. .arg(info.itemId.bare);
  614. const auto modern = Platform::IsWindows10OrGreater();
  615. if (modern) {
  616. toastXml.LoadXml(NotificationTemplate(idString, info.options));
  617. } else {
  618. toastXml = ToastNotificationManager::GetTemplateContent(
  619. (withSubtitle
  620. ? ToastTemplateType::ToastImageAndText04
  621. : ToastTemplateType::ToastImageAndText02));
  622. SetAudioSilent(toastXml);
  623. SetAction(toastXml, idString);
  624. }
  625. const auto userpicKey = info.options.hideNameAndPhoto
  626. ? InMemoryKey()
  627. : peer->userpicUniqueKey(userpicView);
  628. const auto userpicPath = _cachedUserpics.get(
  629. userpicKey,
  630. peer,
  631. userpicView);
  632. const auto userpicPathWide = QDir::toNativeSeparators(
  633. userpicPath).toStdWString();
  634. if (modern && !info.options.hideReplyButton) {
  635. SetReplyIconSrc(toastXml, ensureSendButtonIcon());
  636. SetReplyPlaceholder(
  637. toastXml,
  638. tr::lng_message_ph(tr::now).toStdWString());
  639. }
  640. if (modern && !info.options.hideMarkAsRead) {
  641. SetMarkAsReadText(
  642. toastXml,
  643. tr::lng_context_mark_read(tr::now).toStdWString());
  644. }
  645. SetImageSrc(toastXml, userpicPathWide);
  646. const auto nodeList = toastXml.GetElementsByTagName(L"text");
  647. if (nodeList.Length() < (withSubtitle ? 3U : 2U)) {
  648. return false;
  649. }
  650. SetNodeValueString(
  651. toastXml,
  652. nodeList.Item(0),
  653. info.title.toStdWString());
  654. if (withSubtitle) {
  655. SetNodeValueString(
  656. toastXml,
  657. nodeList.Item(1),
  658. info.subtitle.toStdWString());
  659. }
  660. SetNodeValueString(
  661. toastXml,
  662. nodeList.Item(withSubtitle ? 2 : 1),
  663. info.message.toStdWString());
  664. const auto weak = std::weak_ptr(_guarded);
  665. const auto performOnMainQueue = [=](FnMut<void(Manager *manager)> task) {
  666. crl::on_main(weak, [=, task = std::move(task)]() mutable {
  667. task(*weak.lock());
  668. });
  669. };
  670. auto toast = ToastNotification(toastXml);
  671. const auto token1 = toast.Activated([=](
  672. const ToastNotification &sender,
  673. const winrt::Windows::Foundation::IInspectable &object) {
  674. auto activation = ToastActivation();
  675. const auto string = &ToastActivation::String;
  676. if (const auto args = object.try_as<ToastActivatedEventArgs>()) {
  677. activation.args = string(args.Arguments().c_str());
  678. const auto args2 = args.try_as<IToastActivatedEventArgs2>();
  679. if (!args2 && activation.args.startsWith("action=reply&")) {
  680. LOG(("WinRT Error: "
  681. "FastReply without IToastActivatedEventArgs2 support."));
  682. return;
  683. }
  684. const auto input = args2 ? args2.UserInput() : nullptr;
  685. const auto reply = input
  686. ? input.TryLookup(L"fastReply")
  687. : nullptr;
  688. const auto data = reply
  689. ? reply.try_as<IReference<winrt::hstring>>()
  690. : nullptr;
  691. if (data) {
  692. activation.input.push_back({
  693. .key = u"fastReply"_q,
  694. .value = string(data.GetString().c_str()),
  695. });
  696. }
  697. } else {
  698. activation.args = "action=open&" + idString;
  699. }
  700. crl::on_main([=, activation = std::move(activation)]() mutable {
  701. if (const auto strong = weak.lock()) {
  702. (*strong)->handleActivation(activation);
  703. }
  704. });
  705. });
  706. const auto token2 = toast.Dismissed([=](
  707. const ToastNotification &sender,
  708. const ToastDismissedEventArgs &args) {
  709. const auto reason = args.Reason();
  710. switch (reason) {
  711. case ToastDismissalReason::ApplicationHidden:
  712. case ToastDismissalReason::TimedOut: // Went to Action Center.
  713. break;
  714. case ToastDismissalReason::UserCanceled:
  715. default:
  716. performOnMainQueue([notificationId](Manager *manager) {
  717. manager->clearNotification(notificationId);
  718. });
  719. break;
  720. }
  721. });
  722. const auto token3 = toast.Failed([=](
  723. const ToastNotification &sender,
  724. const ToastFailedEventArgs &args) {
  725. performOnMainQueue([notificationId](Manager *manager) {
  726. manager->clearNotification(notificationId);
  727. });
  728. });
  729. auto i = _notifications.find(key);
  730. if (i != _notifications.cend()) {
  731. auto j = i->second.find(info.itemId);
  732. if (j != i->second.end()) {
  733. const auto existing = j->second;
  734. i->second.erase(j);
  735. tryHide(existing);
  736. i = _notifications.find(key);
  737. }
  738. }
  739. if (i == _notifications.cend()) {
  740. i = _notifications.emplace(
  741. key,
  742. base::flat_map<MsgId, ToastNotification>()).first;
  743. }
  744. if (!base::WinRT::Try([&] { _notifier.Show(toast); })) {
  745. i = _notifications.find(key);
  746. if (i != _notifications.cend() && i->second.empty()) {
  747. _notifications.erase(i);
  748. }
  749. return false;
  750. }
  751. i->second.emplace(info.itemId, toast);
  752. return true;
  753. }
  754. void Manager::Private::tryHide(const ToastNotification &notification) {
  755. base::WinRT::Try([&] {
  756. _notifier.Hide(notification);
  757. });
  758. }
  759. Manager::Manager(Window::Notifications::System *system)
  760. : NativeManager(system)
  761. , _private(std::make_unique<Private>(this)) {
  762. }
  763. bool Manager::init() {
  764. return _private->init();
  765. }
  766. void Manager::clearNotification(NotificationId id) {
  767. _private->clearNotification(id);
  768. }
  769. void Manager::handleActivation(const ToastActivation &activation) {
  770. _private->handleActivation(activation);
  771. }
  772. Manager::~Manager() = default;
  773. void Manager::doShowNativeNotification(
  774. NotificationInfo &&info,
  775. Ui::PeerUserpicView &userpicView) {
  776. _private->showNotification(std::move(info), userpicView);
  777. }
  778. void Manager::doClearAllFast() {
  779. _private->clearAll();
  780. }
  781. void Manager::doClearFromItem(not_null<HistoryItem*> item) {
  782. _private->clearFromItem(item);
  783. }
  784. void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {
  785. _private->clearFromTopic(topic);
  786. }
  787. void Manager::doClearFromHistory(not_null<History*> history) {
  788. _private->clearFromHistory(history);
  789. }
  790. void Manager::doClearFromSession(not_null<Main::Session*> session) {
  791. _private->clearFromSession(session);
  792. }
  793. void Manager::onBeforeNotificationActivated(NotificationId id) {
  794. _private->beforeNotificationActivated(id);
  795. }
  796. void Manager::onAfterNotificationActivated(
  797. NotificationId id,
  798. not_null<Window::SessionController*> window) {
  799. _private->afterNotificationActivated(id, window);
  800. }
  801. bool Manager::doSkipToast() const {
  802. return false;
  803. }
  804. void Manager::doMaybePlaySound(Fn<void()> playSound) {
  805. const auto skip = SkipSoundForCustom()
  806. || QuietHoursEnabled
  807. || FocusAssistBlocks;
  808. if (!skip) {
  809. playSound();
  810. }
  811. }
  812. void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
  813. const auto skip = SkipFlashBounceForCustom()
  814. || QuietHoursEnabled
  815. || FocusAssistBlocks;
  816. if (!skip) {
  817. flashBounce();
  818. }
  819. }
  820. } // namespace Notifications
  821. } // namespace Platform