notifications_manager.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314
  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 "window/notifications_manager.h"
  8. #include "base/options.h"
  9. #include "platform/platform_notifications_manager.h"
  10. #include "window/notifications_manager_default.h"
  11. #include "media/audio/media_audio_track.h"
  12. #include "media/audio/media_audio.h"
  13. #include "mtproto/mtproto_config.h"
  14. #include "history/history.h"
  15. #include "history/history_item_components.h"
  16. #include "history/view/history_view_replies_section.h"
  17. #include "lang/lang_keys.h"
  18. #include "data/notify/data_notify_settings.h"
  19. #include "data/stickers/data_custom_emoji.h"
  20. #include "data/data_document_media.h"
  21. #include "data/data_session.h"
  22. #include "data/data_channel.h"
  23. #include "data/data_forum_topic.h"
  24. #include "data/data_user.h"
  25. #include "data/data_document.h"
  26. #include "data/data_poll.h"
  27. #include "base/unixtime.h"
  28. #include "window/window_controller.h"
  29. #include "window/window_session_controller.h"
  30. #include "core/application.h"
  31. #include "mainwindow.h"
  32. #include "api/api_updates.h"
  33. #include "apiwrap.h"
  34. #include "main/main_account.h"
  35. #include "main/main_session.h"
  36. #include "main/main_domain.h"
  37. #include "ui/text/text_utilities.h"
  38. #include <QtGui/QWindow>
  39. #if __has_include(<gio/gio.hpp>)
  40. #include <gio/gio.hpp>
  41. #endif // __has_include(<gio/gio.hpp>)
  42. namespace Window {
  43. namespace Notifications {
  44. namespace {
  45. // not more than one sound in 500ms from one peer - grouping
  46. constexpr auto kMinimalDelay = crl::time(100);
  47. constexpr auto kMinimalForwardDelay = crl::time(500);
  48. constexpr auto kMinimalAlertDelay = crl::time(500);
  49. constexpr auto kWaitingForAllGroupedDelay = crl::time(1000);
  50. constexpr auto kReactionNotificationEach = 60 * 60 * crl::time(1000);
  51. #ifdef Q_OS_MAC
  52. constexpr auto kSystemAlertDuration = crl::time(1000);
  53. #else // !Q_OS_MAC
  54. constexpr auto kSystemAlertDuration = crl::time(0);
  55. #endif // Q_OS_MAC
  56. [[nodiscard]] QString PlaceholderReactionText() {
  57. static const auto result = QString::fromUtf8("\xf0\x9f\x92\xad");
  58. return result;
  59. }
  60. [[nodiscard]] QString TextWithForwardedChar(
  61. const QString &text,
  62. bool forwarded) {
  63. static const auto result = QString::fromUtf8("\xE2\x9E\xA1\xEF\xB8\x8F");
  64. return forwarded ? result + text : text;
  65. }
  66. [[nodiscard]] QString TextWithPermanentSpoiler(
  67. const TextWithEntities &textWithEntities) {
  68. auto text = textWithEntities.text;
  69. for (const auto &e : textWithEntities.entities) {
  70. if (e.type() == EntityType::Spoiler) {
  71. auto replacement = QString().fill(QChar(0x259A), e.length());
  72. text = text.replace(
  73. e.offset(),
  74. e.length(),
  75. std::move(replacement));
  76. }
  77. }
  78. return text;
  79. }
  80. [[nodiscard]] QByteArray ReadRingtoneBytes(
  81. const std::shared_ptr<Data::DocumentMedia> &media) {
  82. const auto result = media->bytes();
  83. if (!result.isEmpty()) {
  84. return result;
  85. }
  86. const auto &location = media->owner()->location();
  87. if (!location.isEmpty() && location.accessEnable()) {
  88. const auto guard = gsl::finally([&] {
  89. location.accessDisable();
  90. });
  91. auto f = QFile(location.name());
  92. if (f.open(QIODevice::ReadOnly)) {
  93. return f.readAll();
  94. }
  95. }
  96. return {};
  97. }
  98. [[nodiscard]] std::optional<DocumentId> MaybeSoundFor(
  99. not_null<Data::Thread*> thread,
  100. PeerData *from) {
  101. const auto notifySettings = &thread->owner().notifySettings();
  102. const auto threadUnknown = notifySettings->muteUnknown(thread);
  103. const auto threadAlert = !threadUnknown
  104. && !notifySettings->isMuted(thread);
  105. const auto fromUnknown = (!from
  106. || notifySettings->muteUnknown(from));
  107. const auto fromAlert = !fromUnknown
  108. && !notifySettings->isMuted(from);
  109. const auto &sound = notifySettings->sound(thread);
  110. return ((threadAlert || fromAlert) && !sound.none)
  111. ? sound.id
  112. : std::optional<DocumentId>();
  113. }
  114. } // namespace
  115. const char kOptionGNotification[] = "gnotification";
  116. base::options::toggle OptionGNotification({
  117. .id = kOptionGNotification,
  118. .name = "GNotification",
  119. .description = "Force enable GLib's GNotification."
  120. " When disabled, autodetect is used.",
  121. .scope = [] {
  122. #if __has_include(<gio/gio.hpp>)
  123. using namespace gi::repository;
  124. return bool(Gio::Application::get_default());
  125. #else // __has_include(<gio/gio.hpp>)
  126. return false;
  127. #endif // __has_include(<gio/gio.hpp>)
  128. },
  129. .restartRequired = true,
  130. });
  131. struct System::Waiter {
  132. NotificationInHistoryKey key;
  133. UserData *reactionSender = nullptr;
  134. Data::ItemNotificationType type = Data::ItemNotificationType::Message;
  135. crl::time when = 0;
  136. };
  137. System::NotificationInHistoryKey::NotificationInHistoryKey(
  138. Data::ItemNotification notification)
  139. : NotificationInHistoryKey(notification.item->id, notification.type) {
  140. }
  141. System::NotificationInHistoryKey::NotificationInHistoryKey(
  142. MsgId messageId,
  143. Data::ItemNotificationType type)
  144. : messageId(messageId)
  145. , type(type) {
  146. }
  147. System::System()
  148. : _waitTimer([=] { showNext(); })
  149. , _waitForAllGroupedTimer([=] { showGrouped(); })
  150. , _manager(std::make_unique<DummyManager>(this)) {
  151. settingsChanged(
  152. ) | rpl::start_with_next([=](ChangeType type) {
  153. if (type == ChangeType::DesktopEnabled) {
  154. clearAll();
  155. } else if (type == ChangeType::ViewParams) {
  156. updateAll();
  157. } else if (type == ChangeType::IncludeMuted
  158. || type == ChangeType::CountMessages) {
  159. Core::App().domain().notifyUnreadBadgeChanged();
  160. }
  161. }, lifetime());
  162. }
  163. void System::createManager() {
  164. Platform::Notifications::Create(this);
  165. }
  166. void System::setManager(Fn<std::unique_ptr<Manager>()> create) {
  167. Expects(_manager != nullptr);
  168. const auto guard = gsl::finally([&] {
  169. Ensures(_manager != nullptr);
  170. });
  171. if ((Core::App().settings().nativeNotifications()
  172. || Platform::Notifications::Enforced())
  173. && Platform::Notifications::Supported()) {
  174. if (_manager->type() == ManagerType::Native) {
  175. return;
  176. }
  177. if (auto manager = create()) {
  178. _manager = std::move(manager);
  179. return;
  180. }
  181. }
  182. if (Platform::Notifications::Enforced()) {
  183. if (_manager->type() != ManagerType::Dummy) {
  184. _manager = std::make_unique<DummyManager>(this);
  185. }
  186. } else if (_manager->type() != ManagerType::Default) {
  187. _manager = std::make_unique<Default::Manager>(this);
  188. }
  189. }
  190. Main::Session *System::findSession(uint64 sessionId) const {
  191. for (const auto &[index, account] : Core::App().domain().accounts()) {
  192. if (const auto session = account->maybeSession()) {
  193. if (session->uniqueId() == sessionId) {
  194. return session;
  195. }
  196. }
  197. }
  198. return nullptr;
  199. }
  200. bool System::skipReactionNotification(not_null<HistoryItem*> item) const {
  201. const auto id = ReactionNotificationId{
  202. .itemId = item->fullId(),
  203. .sessionId = item->history()->session().uniqueId(),
  204. };
  205. const auto now = crl::now();
  206. const auto clearBefore = now - kReactionNotificationEach;
  207. for (auto i = begin(_sentReactionNotifications)
  208. ; i != end(_sentReactionNotifications)
  209. ;) {
  210. if (i->second <= clearBefore) {
  211. i = _sentReactionNotifications.erase(i);
  212. } else {
  213. ++i;
  214. }
  215. }
  216. return !_sentReactionNotifications.emplace(id, now).second;
  217. }
  218. System::SkipState System::skipNotification(
  219. Data::ItemNotification notification) const {
  220. const auto item = notification.item;
  221. const auto type = notification.type;
  222. const auto messageType = (type == Data::ItemNotificationType::Message);
  223. if (!item->notificationThread()->currentNotification()
  224. || (messageType && item->skipNotification())
  225. || (type == Data::ItemNotificationType::Reaction
  226. && skipReactionNotification(item))) {
  227. return { SkipState::Skip };
  228. }
  229. return computeSkipState(notification);
  230. }
  231. System::SkipState System::computeSkipState(
  232. Data::ItemNotification notification) const {
  233. const auto type = notification.type;
  234. const auto item = notification.item;
  235. const auto thread = item->notificationThread();
  236. const auto notifySettings = &thread->owner().notifySettings();
  237. const auto messageType = (type == Data::ItemNotificationType::Message);
  238. const auto withSilent = [&](
  239. SkipState::Value value,
  240. bool forceSilent = false) {
  241. return SkipState{
  242. .value = value,
  243. .silent = (forceSilent
  244. || !messageType
  245. || item->isSilent()
  246. || notifySettings->sound(thread).none),
  247. };
  248. };
  249. const auto showForMuted = messageType
  250. && item->out()
  251. && item->isFromScheduled();
  252. const auto notifyBy = messageType
  253. ? item->specialNotificationPeer()
  254. : notification.reactionSender;
  255. if (Core::Quitting()) {
  256. return { SkipState::Skip };
  257. } else if (!Core::App().settings().notifyFromAll()
  258. && &thread->session().account() != &Core::App().domain().active()) {
  259. return { SkipState::Skip };
  260. }
  261. if (messageType) {
  262. notifySettings->request(thread);
  263. } else if (notifyBy->blockStatus() == PeerData::BlockStatus::Unknown) {
  264. notifyBy->updateFull();
  265. }
  266. if (notifyBy) {
  267. notifySettings->request(notifyBy);
  268. }
  269. if (messageType && notifySettings->muteUnknown(thread)) {
  270. return { SkipState::Unknown };
  271. } else if (messageType && !notifySettings->isMuted(thread)) {
  272. return withSilent(SkipState::DontSkip);
  273. } else if (!notifyBy) {
  274. return withSilent(
  275. showForMuted ? SkipState::DontSkip : SkipState::Skip,
  276. showForMuted);
  277. } else if (notifySettings->muteUnknown(notifyBy)
  278. || (!messageType
  279. && notifyBy->blockStatus() == PeerData::BlockStatus::Unknown)) {
  280. return withSilent(SkipState::Unknown);
  281. } else if (!notifySettings->isMuted(notifyBy)
  282. && (messageType || !notifyBy->isBlocked())) {
  283. return withSilent(SkipState::DontSkip);
  284. } else {
  285. return withSilent(
  286. showForMuted ? SkipState::DontSkip : SkipState::Skip,
  287. showForMuted);
  288. }
  289. }
  290. System::Timing System::countTiming(
  291. not_null<Data::Thread*> thread,
  292. crl::time minimalDelay) const {
  293. auto delay = minimalDelay;
  294. const auto t = base::unixtime::now();
  295. const auto ms = crl::now();
  296. const auto &updates = thread->session().updates();
  297. const auto &config = thread->session().serverConfig();
  298. const bool isOnline = updates.lastWasOnline();
  299. const auto otherNotOld = ((cOtherOnline() * 1000LL) + config.onlineCloudTimeout > t * 1000LL);
  300. const bool otherLaterThanMe = (cOtherOnline() * 1000LL + (ms - updates.lastSetOnline()) > t * 1000LL);
  301. if (!isOnline && otherNotOld && otherLaterThanMe) {
  302. delay = config.notifyCloudDelay;
  303. } else if (cOtherOnline() >= t) {
  304. delay = config.notifyDefaultDelay;
  305. }
  306. return {
  307. .delay = delay,
  308. .when = ms + delay,
  309. };
  310. }
  311. void System::registerThread(not_null<Data::Thread*> thread) {
  312. if (const auto topic = thread->asTopic()) {
  313. const auto &[i, ok] = _watchedTopics.emplace(topic, rpl::lifetime());
  314. if (ok) {
  315. topic->destroyed() | rpl::start_with_next([=] {
  316. clearFromTopic(topic);
  317. }, i->second);
  318. }
  319. }
  320. }
  321. void System::schedule(Data::ItemNotification notification) {
  322. Expects(_manager != nullptr);
  323. const auto item = notification.item;
  324. const auto type = notification.type;
  325. const auto thread = item->notificationThread();
  326. const auto skip = skipNotification(notification);
  327. if (skip.value == SkipState::Skip) {
  328. thread->popNotification(notification);
  329. return;
  330. }
  331. const auto ready = (skip.value != SkipState::Unknown)
  332. && item->notificationReady();
  333. const auto minimalDelay = (type == Data::ItemNotificationType::Reaction)
  334. ? kMinimalDelay
  335. : item->Has<HistoryMessageForwarded>()
  336. ? kMinimalForwardDelay
  337. : kMinimalDelay;
  338. const auto timing = countTiming(thread, minimalDelay);
  339. const auto notifyBy = (type == Data::ItemNotificationType::Message)
  340. ? item->specialNotificationPeer()
  341. : notification.reactionSender;
  342. if (!skip.silent) {
  343. registerThread(thread);
  344. _whenAlerts[thread].emplace(timing.when, notifyBy);
  345. }
  346. if (const auto user = item->history()->peer->asUser()) {
  347. if (user->hasStarsPerMessage()
  348. && !user->messageMoneyRestrictionsKnown()) {
  349. user->updateFull();
  350. }
  351. }
  352. if (Core::App().settings().desktopNotify()
  353. && !_manager->skipToast()) {
  354. registerThread(thread);
  355. const auto key = NotificationInHistoryKey(notification);
  356. auto &whenMap = _whenMaps[thread];
  357. if (whenMap.find(key) == whenMap.end()) {
  358. whenMap.emplace(key, timing.when);
  359. }
  360. auto &addTo = ready ? _waiters : _settingWaiters;
  361. const auto it = addTo.find(thread);
  362. if (it == addTo.end() || it->second.when > timing.when) {
  363. addTo.emplace(thread, Waiter{
  364. .key = key,
  365. .reactionSender = notification.reactionSender,
  366. .type = notification.type,
  367. .when = timing.when,
  368. });
  369. }
  370. }
  371. if (ready) {
  372. if (!_waitTimer.isActive()
  373. || _waitTimer.remainingTime() > timing.delay) {
  374. _waitTimer.callOnce(timing.delay);
  375. }
  376. }
  377. }
  378. void System::clearAll() {
  379. if (_manager) {
  380. _manager->clearAll();
  381. }
  382. for (const auto &[thread, _] : _whenMaps) {
  383. thread->clearNotifications();
  384. }
  385. _whenMaps.clear();
  386. _whenAlerts.clear();
  387. _waiters.clear();
  388. _settingWaiters.clear();
  389. _watchedTopics.clear();
  390. }
  391. void System::clearFromTopic(not_null<Data::ForumTopic*> topic) {
  392. if (_manager) {
  393. _manager->clearFromTopic(topic);
  394. }
  395. topic->clearNotifications();
  396. _whenMaps.remove(topic);
  397. _whenAlerts.remove(topic);
  398. _waiters.remove(topic);
  399. _settingWaiters.remove(topic);
  400. _watchedTopics.remove(topic);
  401. _waitTimer.cancel();
  402. showNext();
  403. }
  404. void System::clearForThreadIf(Fn<bool(not_null<Data::Thread*>)> predicate) {
  405. for (auto i = _whenMaps.begin(); i != _whenMaps.end();) {
  406. const auto thread = i->first;
  407. if (!predicate(thread)) {
  408. ++i;
  409. continue;
  410. }
  411. i = _whenMaps.erase(i);
  412. thread->clearNotifications();
  413. _whenAlerts.remove(thread);
  414. _waiters.remove(thread);
  415. _settingWaiters.remove(thread);
  416. if (const auto topic = thread->asTopic()) {
  417. _watchedTopics.remove(topic);
  418. }
  419. }
  420. const auto clearFrom = [&](auto &map) {
  421. for (auto i = map.begin(); i != map.end();) {
  422. const auto thread = i->first;
  423. if (predicate(thread)) {
  424. if (const auto topic = thread->asTopic()) {
  425. _watchedTopics.remove(topic);
  426. }
  427. i = map.erase(i);
  428. } else {
  429. ++i;
  430. }
  431. }
  432. };
  433. clearFrom(_whenAlerts);
  434. clearFrom(_waiters);
  435. clearFrom(_settingWaiters);
  436. _waitTimer.cancel();
  437. showNext();
  438. }
  439. void System::clearFromHistory(not_null<History*> history) {
  440. if (_manager) {
  441. _manager->clearFromHistory(history);
  442. }
  443. clearForThreadIf([&](not_null<Data::Thread*> thread) {
  444. return (thread->owningHistory() == history);
  445. });
  446. }
  447. void System::clearFromSession(not_null<Main::Session*> session) {
  448. if (_manager) {
  449. _manager->clearFromSession(session);
  450. }
  451. clearForThreadIf([&](not_null<Data::Thread*> thread) {
  452. return (&thread->session() == session);
  453. });
  454. }
  455. void System::clearIncomingFromHistory(not_null<History*> history) {
  456. if (_manager) {
  457. _manager->clearFromHistory(history);
  458. }
  459. history->clearIncomingNotifications();
  460. _whenAlerts.remove(history);
  461. }
  462. void System::clearIncomingFromTopic(not_null<Data::ForumTopic*> topic) {
  463. if (_manager) {
  464. _manager->clearFromTopic(topic);
  465. }
  466. topic->clearIncomingNotifications();
  467. _whenAlerts.remove(topic);
  468. }
  469. void System::clearFromItem(not_null<HistoryItem*> item) {
  470. if (_manager) {
  471. _manager->clearFromItem(item);
  472. }
  473. }
  474. void System::clearAllFast() {
  475. if (_manager) {
  476. _manager->clearAllFast();
  477. }
  478. _whenMaps.clear();
  479. _whenAlerts.clear();
  480. _waiters.clear();
  481. _settingWaiters.clear();
  482. _watchedTopics.clear();
  483. }
  484. void System::checkDelayed() {
  485. for (auto i = _settingWaiters.begin(); i != _settingWaiters.end();) {
  486. const auto remove = [&] {
  487. const auto thread = i->first;
  488. const auto peer = thread->peer();
  489. const auto fullId = FullMsgId(peer->id, i->second.key.messageId);
  490. const auto item = thread->owner().message(fullId);
  491. if (!item) {
  492. return true;
  493. }
  494. const auto state = computeSkipState({
  495. .item = item,
  496. .reactionSender = i->second.reactionSender,
  497. .type = i->second.type,
  498. });
  499. if (state.value == SkipState::Skip) {
  500. return true;
  501. } else if (state.value == SkipState::Unknown
  502. || !item->notificationReady()) {
  503. return false;
  504. }
  505. _waiters.emplace(i->first, i->second);
  506. return true;
  507. }();
  508. if (remove) {
  509. i = _settingWaiters.erase(i);
  510. } else {
  511. ++i;
  512. }
  513. }
  514. _waitTimer.cancel();
  515. showNext();
  516. }
  517. void System::showGrouped() {
  518. Expects(_manager != nullptr);
  519. if (const auto session = findSession(_lastHistorySessionId)) {
  520. if (const auto lastItem = session->data().message(_lastHistoryItemId)) {
  521. _waitForAllGroupedTimer.cancel();
  522. _manager->showNotification({
  523. .item = lastItem,
  524. .forwardedCount = _lastForwardedCount,
  525. .soundId = _lastSoundId,
  526. });
  527. _lastForwardedCount = 0;
  528. _lastHistoryItemId = FullMsgId();
  529. _lastHistorySessionId = 0;
  530. _lastSoundId = {};
  531. }
  532. }
  533. }
  534. void System::showNext() {
  535. Expects(_manager != nullptr);
  536. if (Core::Quitting()) {
  537. return;
  538. }
  539. const auto isSameGroup = [=](HistoryItem *item) {
  540. if (!_lastHistorySessionId || !_lastHistoryItemId || !item) {
  541. return false;
  542. } else if (item->history()->session().uniqueId()
  543. != _lastHistorySessionId) {
  544. return false;
  545. }
  546. const auto lastItem = item->history()->owner().message(
  547. _lastHistoryItemId);
  548. if (lastItem) {
  549. return (lastItem->groupId() == item->groupId())
  550. || (lastItem->author() == item->author());
  551. }
  552. return false;
  553. };
  554. auto ms = crl::now(), nextAlert = crl::time(0);
  555. auto alertThread = (Data::Thread*)nullptr;
  556. auto alertSoundId = std::optional<DocumentId>();
  557. for (auto i = _whenAlerts.begin(); i != _whenAlerts.end();) {
  558. while (!i->second.empty() && i->second.begin()->first <= ms) {
  559. const auto thread = i->first;
  560. const auto from = i->second.begin()->second;
  561. if (const auto soundId = MaybeSoundFor(thread, from)) {
  562. alertThread = thread;
  563. alertSoundId = soundId;
  564. }
  565. while (!i->second.empty()
  566. && i->second.begin()->first <= ms + kMinimalAlertDelay) {
  567. i->second.erase(i->second.begin());
  568. }
  569. }
  570. if (i->second.empty()) {
  571. i = _whenAlerts.erase(i);
  572. } else {
  573. if (!nextAlert || nextAlert > i->second.begin()->first) {
  574. nextAlert = i->second.begin()->first;
  575. }
  576. ++i;
  577. }
  578. }
  579. const auto &settings = Core::App().settings();
  580. if (alertThread) {
  581. if (settings.flashBounceNotify()) {
  582. const auto peer = alertThread->peer();
  583. if (const auto window = Core::App().windowFor(peer)) {
  584. if (const auto controller = window->sessionController()) {
  585. _manager->maybeFlashBounce(crl::guard(controller, [=] {
  586. if (const auto handle = window->widget()->windowHandle()) {
  587. handle->alert(kSystemAlertDuration);
  588. // (handle, SLOT(_q_clearAlert())); in the future.
  589. }
  590. }));
  591. }
  592. }
  593. }
  594. if (settings.soundNotify()) {
  595. const auto owner = &alertThread->owner();
  596. const auto id = owner->notifySettings().sound(alertThread).id;
  597. _manager->maybePlaySound(crl::guard(&owner->session(), [=] {
  598. const auto track = lookupSound(owner, id);
  599. track->playOnce();
  600. Media::Player::mixer()->suppressAll(track->getLengthMs());
  601. Media::Player::mixer()->scheduleFaderCallback();
  602. }));
  603. }
  604. }
  605. if (_waiters.empty()
  606. || !settings.desktopNotify()
  607. || _manager->skipToast()) {
  608. if (nextAlert) {
  609. _waitTimer.callOnce(nextAlert - ms);
  610. }
  611. return;
  612. }
  613. while (true) {
  614. auto next = 0LL;
  615. auto notify = std::optional<Data::ItemNotification>();
  616. auto notifyThread = (Data::Thread*)nullptr;
  617. for (auto i = _waiters.begin(); i != _waiters.end();) {
  618. const auto thread = i->first;
  619. auto current = thread->currentNotification();
  620. if (current && current->item->id != i->second.key.messageId) {
  621. auto j = _whenMaps.find(thread);
  622. if (j == _whenMaps.end()) {
  623. thread->clearNotifications();
  624. i = _waiters.erase(i);
  625. continue;
  626. }
  627. do {
  628. auto k = j->second.find(*current);
  629. if (k != j->second.cend()) {
  630. i->second.key = k->first;
  631. i->second.when = k->second;
  632. break;
  633. }
  634. thread->skipNotification();
  635. current = thread->currentNotification();
  636. } while (current);
  637. }
  638. if (!current) {
  639. _whenMaps.remove(thread);
  640. i = _waiters.erase(i);
  641. continue;
  642. }
  643. auto when = i->second.when;
  644. if (!notify || next > when) {
  645. next = when;
  646. notify = current,
  647. notifyThread = thread;
  648. }
  649. ++i;
  650. }
  651. if (!notify) {
  652. break;
  653. } else if (next > ms) {
  654. if (nextAlert && nextAlert < next) {
  655. next = nextAlert;
  656. nextAlert = 0;
  657. }
  658. _waitTimer.callOnce(next - ms);
  659. break;
  660. }
  661. const auto notifyItem = notify->item;
  662. const auto notifySilent = computeSkipState(*notify).silent;
  663. const auto messageType = (notify->type
  664. == Data::ItemNotificationType::Message);
  665. const auto isForwarded = messageType
  666. && notifyItem->Has<HistoryMessageForwarded>();
  667. const auto isAlbum = messageType
  668. && notifyItem->groupId();
  669. // Forwarded and album notify grouping.
  670. auto groupedItem = (isForwarded || isAlbum)
  671. ? notifyItem.get()
  672. : nullptr;
  673. auto forwardedCount = isForwarded ? 1 : 0;
  674. const auto thread = notifyItem->notificationThread();
  675. const auto j = _whenMaps.find(thread);
  676. if (j == _whenMaps.cend()) {
  677. thread->clearNotifications();
  678. } else {
  679. while (true) {
  680. auto nextNotify = std::optional<Data::ItemNotification>();
  681. thread->skipNotification();
  682. if (!thread->hasNotification()) {
  683. break;
  684. }
  685. j->second.remove({
  686. (groupedItem ? groupedItem : notifyItem.get())->id,
  687. notify->type,
  688. });
  689. do {
  690. const auto k = j->second.find(
  691. thread->currentNotification());
  692. if (k != j->second.cend()) {
  693. nextNotify = thread->currentNotification();
  694. _waiters.emplace(notifyThread, Waiter{
  695. .key = k->first,
  696. .when = k->second
  697. });
  698. break;
  699. }
  700. thread->skipNotification();
  701. } while (thread->hasNotification());
  702. if (!nextNotify || !groupedItem) {
  703. break;
  704. }
  705. const auto nextMessageNotification
  706. = (nextNotify->type
  707. == Data::ItemNotificationType::Message);
  708. const auto canNextBeGrouped = nextMessageNotification
  709. && ((isForwarded
  710. && nextNotify->item->Has<HistoryMessageForwarded>())
  711. || (isAlbum && nextNotify->item->groupId()));
  712. const auto nextItem = canNextBeGrouped
  713. ? nextNotify->item.get()
  714. : nullptr;
  715. if (nextItem
  716. && qAbs(int64(nextItem->date()) - int64(groupedItem->date())) < 2) {
  717. if (isForwarded
  718. && groupedItem->author() == nextItem->author()) {
  719. ++forwardedCount;
  720. groupedItem = nextItem;
  721. continue;
  722. }
  723. if (isAlbum
  724. && groupedItem->groupId() == nextItem->groupId()) {
  725. groupedItem = nextItem;
  726. continue;
  727. }
  728. }
  729. break;
  730. }
  731. }
  732. if (!_lastHistoryItemId && groupedItem) {
  733. _lastHistorySessionId = groupedItem->history()->session().uniqueId();
  734. _lastHistoryItemId = groupedItem->fullId();
  735. _lastSoundId = notifySilent ? std::nullopt : MaybeSoundFor(
  736. notifyThread,
  737. groupedItem->specialNotificationPeer());
  738. }
  739. // If the current notification is grouped.
  740. if (isAlbum || isForwarded) {
  741. // If the previous notification is grouped
  742. // then reset the timer.
  743. if (_waitForAllGroupedTimer.isActive()) {
  744. _waitForAllGroupedTimer.cancel();
  745. // If this is not the same group
  746. // then show the previous group immediately.
  747. if (!isSameGroup(groupedItem)) {
  748. showGrouped();
  749. }
  750. }
  751. // We have to wait until all the messages in this group are loaded.
  752. _lastForwardedCount += forwardedCount;
  753. _lastHistorySessionId = groupedItem->history()->session().uniqueId();
  754. _lastHistoryItemId = groupedItem->fullId();
  755. _lastSoundId = notifySilent ? std::nullopt : MaybeSoundFor(
  756. notifyThread,
  757. groupedItem->specialNotificationPeer());
  758. _waitForAllGroupedTimer.callOnce(kWaitingForAllGroupedDelay);
  759. } else {
  760. // If the current notification is not grouped
  761. // then there is no reason to wait for the timer
  762. // to show the previous notification.
  763. showGrouped();
  764. const auto reactionNotification
  765. = (notify->type == Data::ItemNotificationType::Reaction);
  766. const auto reaction = reactionNotification
  767. ? notify->item->lookupUnreadReaction(notify->reactionSender)
  768. : Data::ReactionId();
  769. const auto soundFrom = reactionNotification
  770. ? notify->reactionSender
  771. : notify->item->specialNotificationPeer();
  772. if (!reactionNotification || !reaction.empty()) {
  773. _manager->showNotification({
  774. .item = notify->item,
  775. .forwardedCount = forwardedCount,
  776. .reactionFrom = notify->reactionSender,
  777. .reactionId = reaction,
  778. .soundId = (notifySilent
  779. ? std::nullopt
  780. : MaybeSoundFor(notifyThread, soundFrom)),
  781. });
  782. }
  783. }
  784. if (!thread->hasNotification()) {
  785. _waiters.remove(thread);
  786. _whenMaps.remove(thread);
  787. }
  788. }
  789. if (nextAlert) {
  790. _waitTimer.callOnce(nextAlert - ms);
  791. }
  792. }
  793. QByteArray System::lookupSoundBytes(
  794. not_null<Data::Session*> owner,
  795. DocumentId id) {
  796. if (id) {
  797. const auto &notifySettings = owner->notifySettings();
  798. const auto custom = notifySettings.lookupRingtone(id);
  799. return custom ? ReadRingtoneBytes(custom) : QByteArray();
  800. }
  801. auto f = QFile(Core::App().settings().getSoundPath(u"msg_incoming"_q));
  802. if (f.open(QIODevice::ReadOnly)) {
  803. return f.readAll();
  804. }
  805. auto fallback = QFile(u":/sounds/msg_incoming.mp3"_q);
  806. if (fallback.open(QIODevice::ReadOnly)) {
  807. return fallback.readAll();
  808. }
  809. Unexpected("Embedded sound not found!");
  810. }
  811. not_null<Media::Audio::Track*> System::lookupSound(
  812. not_null<Data::Session*> owner,
  813. DocumentId id) {
  814. if (!id) {
  815. ensureSoundCreated();
  816. return _soundTrack.get();
  817. }
  818. const auto i = _customSoundTracks.find(id);
  819. if (i != end(_customSoundTracks)) {
  820. return i->second.get();
  821. }
  822. const auto bytes = lookupSoundBytes(owner, id);
  823. if (!bytes.isEmpty()) {
  824. const auto j = _customSoundTracks.emplace(
  825. id,
  826. Media::Audio::Current().createTrack()
  827. ).first;
  828. j->second->fillFromData(bytes::make_vector(bytes));
  829. return j->second.get();
  830. }
  831. ensureSoundCreated();
  832. return _soundTrack.get();
  833. }
  834. void System::ensureSoundCreated() {
  835. if (_soundTrack) {
  836. return;
  837. }
  838. _soundTrack = Media::Audio::Current().createTrack();
  839. _soundTrack->fillFromFile(
  840. Core::App().settings().getSoundPath(u"msg_incoming"_q));
  841. }
  842. void System::updateAll() {
  843. if (_manager) {
  844. _manager->updateAll();
  845. }
  846. }
  847. rpl::producer<ChangeType> System::settingsChanged() const {
  848. return _settingsChanged.events();
  849. }
  850. void System::notifySettingsChanged(ChangeType type) {
  851. return _settingsChanged.fire(std::move(type));
  852. }
  853. void System::playSound(not_null<Main::Session*> session, DocumentId id) {
  854. lookupSound(&session->data(), id)->playOnce();
  855. }
  856. Manager::DisplayOptions Manager::getNotificationOptions(
  857. HistoryItem *item,
  858. Data::ItemNotificationType type) const {
  859. const auto hideEverything = Core::App().passcodeLocked()
  860. || forceHideDetails();
  861. const auto view = Core::App().settings().notifyView();
  862. const auto peer = item ? item->history()->peer.get() : nullptr;
  863. const auto topic = item ? item->topic() : nullptr;
  864. auto result = DisplayOptions();
  865. result.hideNameAndPhoto = hideEverything
  866. || (view > Core::Settings::NotifyView::ShowName);
  867. result.hideMessageText = hideEverything
  868. || (view > Core::Settings::NotifyView::ShowPreview);
  869. result.hideMarkAsRead = result.hideMessageText
  870. || (type != Data::ItemNotificationType::Message)
  871. || !item
  872. || ((item->out() || peer->isSelf()) && item->isFromScheduled());
  873. result.hideReplyButton = result.hideMarkAsRead
  874. || (!Data::CanSendTexts(peer)
  875. && (!topic || !Data::CanSendTexts(topic)))
  876. || peer->isBroadcast()
  877. || (peer->slowmodeSecondsLeft() > 0)
  878. || (peer->starsPerMessageChecked() > 0);
  879. result.spoilerLoginCode = item
  880. && !item->out()
  881. && peer->isNotificationsUser()
  882. && Core::App().isSharingScreen();
  883. return result;
  884. }
  885. TextWithEntities Manager::ComposeReactionEmoji(
  886. not_null<Main::Session*> session,
  887. const Data::ReactionId &reaction) {
  888. if (const auto emoji = std::get_if<QString>(&reaction.data)) {
  889. return TextWithEntities{ *emoji };
  890. }
  891. const auto id = v::get<DocumentId>(reaction.data);
  892. const auto document = session->data().document(id);
  893. const auto sticker = document->sticker();
  894. const auto text = sticker ? sticker->alt : PlaceholderReactionText();
  895. return TextWithEntities{
  896. text,
  897. {
  898. EntityInText(
  899. EntityType::CustomEmoji,
  900. 0,
  901. text.size(),
  902. Data::SerializeCustomEmojiId(id))
  903. }
  904. };
  905. }
  906. TextWithEntities Manager::ComposeReactionNotification(
  907. not_null<HistoryItem*> item,
  908. const Data::ReactionId &reaction,
  909. bool hideContent) {
  910. const auto reactionWithEntities = ComposeReactionEmoji(
  911. &item->history()->session(),
  912. reaction);
  913. const auto simple = [&](const auto &phrase) {
  914. return phrase(
  915. tr::now,
  916. lt_reaction,
  917. reactionWithEntities,
  918. Ui::Text::WithEntities);
  919. };
  920. if (hideContent) {
  921. return simple(tr::lng_reaction_notext);
  922. }
  923. const auto media = item->media();
  924. const auto text = [&] {
  925. return tr::lng_reaction_text(
  926. tr::now,
  927. lt_reaction,
  928. reactionWithEntities,
  929. lt_text,
  930. item->notificationText(),
  931. Ui::Text::WithEntities);
  932. };
  933. if (!media || media->webpage()) {
  934. return text();
  935. } else if (media->photo()) {
  936. return simple(tr::lng_reaction_photo);
  937. } else if (const auto document = media->document()) {
  938. if (document->isVoiceMessage()) {
  939. return simple(tr::lng_reaction_voice_message);
  940. } else if (document->isVideoMessage()) {
  941. return simple(tr::lng_reaction_video_message);
  942. } else if (document->isAnimation()) {
  943. return simple(tr::lng_reaction_gif);
  944. } else if (document->isVideoFile()) {
  945. return simple(tr::lng_reaction_video);
  946. } else if (const auto sticker = document->sticker()) {
  947. return tr::lng_reaction_sticker(
  948. tr::now,
  949. lt_reaction,
  950. reactionWithEntities,
  951. lt_emoji,
  952. Ui::Text::WithEntities(sticker->alt),
  953. Ui::Text::WithEntities);
  954. }
  955. return simple(tr::lng_reaction_document);
  956. } else if (const auto contact = media->sharedContact()) {
  957. const auto name = contact->firstName.isEmpty()
  958. ? contact->lastName
  959. : contact->lastName.isEmpty()
  960. ? contact->firstName
  961. : tr::lng_full_name(
  962. tr::now,
  963. lt_first_name,
  964. contact->firstName,
  965. lt_last_name,
  966. contact->lastName);
  967. return tr::lng_reaction_contact(
  968. tr::now,
  969. lt_reaction,
  970. reactionWithEntities,
  971. lt_name,
  972. Ui::Text::WithEntities(name),
  973. Ui::Text::WithEntities);
  974. } else if (media->location()) {
  975. return simple(tr::lng_reaction_location);
  976. // lng_reaction_live_location not used right now :(
  977. } else if (const auto poll = media->poll()) {
  978. return (poll->quiz()
  979. ? tr::lng_reaction_quiz
  980. : tr::lng_reaction_poll)(
  981. tr::now,
  982. lt_reaction,
  983. reactionWithEntities,
  984. lt_title,
  985. poll->question,
  986. Ui::Text::WithEntities);
  987. } else if (media->game()) {
  988. return simple(tr::lng_reaction_game);
  989. } else if (media->invoice()) {
  990. return simple(tr::lng_reaction_invoice);
  991. }
  992. return text();
  993. }
  994. TextWithEntities Manager::addTargetAccountName(
  995. TextWithEntities title,
  996. not_null<Main::Session*> session) {
  997. const auto add = [&] {
  998. for (const auto &[index, account] : Core::App().domain().accounts()) {
  999. if (const auto other = account->maybeSession()) {
  1000. if (other != session) {
  1001. return true;
  1002. }
  1003. }
  1004. }
  1005. return false;
  1006. }();
  1007. if (!add) {
  1008. return title;
  1009. }
  1010. return title.append(accountNameSeparator()).append(
  1011. (session->user()->username().isEmpty()
  1012. ? session->user()->name()
  1013. : session->user()->username()));
  1014. }
  1015. QString Manager::addTargetAccountName(
  1016. const QString &title,
  1017. not_null<Main::Session*> session) {
  1018. return addTargetAccountName(TextWithEntities{ title }, session).text;
  1019. }
  1020. QString Manager::accountNameSeparator() {
  1021. return QString::fromUtf8(" \xE2\x9E\x9C ");
  1022. }
  1023. void Manager::notificationActivated(
  1024. NotificationId id,
  1025. const TextWithTags &reply) {
  1026. onBeforeNotificationActivated(id);
  1027. if (const auto session = system()->findSession(id.contextId.sessionId)) {
  1028. if (session->windows().empty()) {
  1029. Core::App().domain().activate(&session->account());
  1030. }
  1031. if (!session->windows().empty()) {
  1032. const auto window = session->windows().front();
  1033. const auto history = session->data().history(
  1034. id.contextId.peerId);
  1035. const auto item = history->owner().message(
  1036. history->peer,
  1037. id.msgId);
  1038. const auto topic = item ? item->topic() : nullptr;
  1039. if (!reply.text.isEmpty()) {
  1040. const auto topicRootId = topic
  1041. ? topic->rootId()
  1042. : id.contextId.topicRootId;
  1043. const auto replyToId = (id.msgId > 0
  1044. && !history->peer->isUser()
  1045. && id.msgId != topicRootId)
  1046. ? FullMsgId(history->peer->id, id.msgId)
  1047. : FullMsgId();
  1048. auto draft = std::make_unique<Data::Draft>(
  1049. reply,
  1050. FullReplyTo{
  1051. .messageId = replyToId,
  1052. .topicRootId = topicRootId,
  1053. },
  1054. MessageCursor{
  1055. int(reply.text.size()),
  1056. int(reply.text.size()),
  1057. Ui::kQFixedMax,
  1058. },
  1059. Data::WebPageDraft());
  1060. history->setLocalDraft(std::move(draft));
  1061. }
  1062. window->widget()->showFromTray();
  1063. if (Core::App().passcodeLocked()) {
  1064. window->widget()->setInnerFocus();
  1065. system()->clearAll();
  1066. } else {
  1067. openNotificationMessage(history, id.msgId);
  1068. }
  1069. onAfterNotificationActivated(id, window);
  1070. }
  1071. }
  1072. }
  1073. void Manager::openNotificationMessage(
  1074. not_null<History*> history,
  1075. MsgId messageId) {
  1076. const auto item = history->owner().message(history->peer, messageId);
  1077. const auto openExactlyMessage = !history->peer->isBroadcast()
  1078. && item
  1079. && item->isRegular()
  1080. && (item->out() || (item->mentionsMe() && !history->peer->isUser()));
  1081. const auto topic = item ? item->topic() : nullptr;
  1082. const auto separate = Core::App().separateWindowFor(history->peer);
  1083. const auto window = separate
  1084. ? separate->sessionController()
  1085. : history->session().tryResolveWindow();
  1086. const auto itemId = openExactlyMessage ? messageId : ShowAtUnreadMsgId;
  1087. if (window) {
  1088. if (topic) {
  1089. window->showSection(
  1090. std::make_shared<HistoryView::RepliesMemento>(
  1091. history,
  1092. topic->rootId(),
  1093. itemId),
  1094. SectionShow::Way::Forward);
  1095. } else {
  1096. window->showPeerHistory(
  1097. history->peer->id,
  1098. SectionShow::Way::Forward,
  1099. itemId);
  1100. }
  1101. }
  1102. if (topic) {
  1103. system()->clearFromTopic(topic);
  1104. } else {
  1105. system()->clearFromHistory(history);
  1106. }
  1107. }
  1108. void Manager::notificationReplied(
  1109. NotificationId id,
  1110. const TextWithTags &reply) {
  1111. if (!id.contextId.sessionId || !id.contextId.peerId) {
  1112. return;
  1113. }
  1114. const auto session = system()->findSession(id.contextId.sessionId);
  1115. if (!session) {
  1116. return;
  1117. }
  1118. const auto history = session->data().history(id.contextId.peerId);
  1119. const auto item = history->owner().message(history->peer, id.msgId);
  1120. const auto topic = item ? item->topic() : nullptr;
  1121. const auto topicRootId = topic
  1122. ? topic->rootId()
  1123. : id.contextId.topicRootId;
  1124. auto message = Api::MessageToSend(Api::SendAction(history));
  1125. message.textWithTags = reply;
  1126. const auto replyToId = (id.msgId > 0 && !history->peer->isUser()
  1127. && id.msgId != topicRootId)
  1128. ? id.msgId
  1129. : history->peer->isForum()
  1130. ? topicRootId
  1131. : MsgId(0);
  1132. message.action.replyTo = {
  1133. .messageId = { replyToId ? history->peer->id : 0, replyToId },
  1134. .topicRootId = topic ? topic->rootId() : 0,
  1135. };
  1136. message.action.clearDraft = false;
  1137. history->session().api().sendMessage(std::move(message));
  1138. if (item && item->isUnreadMention() && !item->isIncomingUnreadMedia()) {
  1139. history->session().api().markContentsRead(item);
  1140. }
  1141. }
  1142. void NativeManager::doShowNotification(NotificationFields &&fields) {
  1143. const auto options = getNotificationOptions(
  1144. fields.item,
  1145. (fields.reactionFrom
  1146. ? Data::ItemNotificationType::Reaction
  1147. : Data::ItemNotificationType::Message));
  1148. const auto item = fields.item;
  1149. const auto peer = item->history()->peer;
  1150. const auto reactionFrom = fields.reactionFrom;
  1151. if (reactionFrom && options.hideNameAndPhoto) {
  1152. return;
  1153. }
  1154. const auto scheduled = !options.hideNameAndPhoto
  1155. && !reactionFrom
  1156. && (item->out() || peer->isSelf())
  1157. && item->isFromScheduled();
  1158. const auto topicWithChat = [&] {
  1159. const auto name = peer->name();
  1160. const auto topic = item->topic();
  1161. return topic ? (topic->title() + u" ("_q + name + ')') : name;
  1162. };
  1163. const auto title = options.hideNameAndPhoto
  1164. ? AppName.utf16()
  1165. : (scheduled && peer->isSelf())
  1166. ? tr::lng_notification_reminder(tr::now)
  1167. : topicWithChat();
  1168. const auto fullTitle = addTargetAccountName(title, &peer->session());
  1169. const auto subtitle = reactionFrom
  1170. ? (reactionFrom != peer ? reactionFrom->name() : QString())
  1171. : options.hideNameAndPhoto
  1172. ? QString()
  1173. : item->notificationHeader();
  1174. const auto text = reactionFrom
  1175. ? TextWithPermanentSpoiler(ComposeReactionNotification(
  1176. item,
  1177. fields.reactionId,
  1178. options.hideMessageText))
  1179. : options.hideMessageText
  1180. ? tr::lng_notification_preview(tr::now)
  1181. : (fields.forwardedCount > 1)
  1182. ? tr::lng_forward_messages(tr::now, lt_count, fields.forwardedCount)
  1183. : item->groupId()
  1184. ? tr::lng_in_dlg_album(tr::now)
  1185. : TextWithForwardedChar(
  1186. TextWithPermanentSpoiler(item->notificationText({
  1187. .spoilerLoginCode = options.spoilerLoginCode,
  1188. })),
  1189. (fields.forwardedCount == 1));
  1190. // #TODO optimize
  1191. auto userpicView = item->history()->peer->createUserpicView();
  1192. const auto owner = &item->history()->owner();
  1193. const auto withSound = fields.soundId
  1194. && Core::App().settings().soundNotify();
  1195. const auto sound = withSound ? [=, id = *fields.soundId] {
  1196. return _localSoundCache.sound(id, [=] {
  1197. return Core::App().notifications().lookupSoundBytes(owner, id);
  1198. }, [=] {
  1199. return Core::App().notifications().lookupSoundBytes(owner, 0);
  1200. });
  1201. } : Fn<NotificationSound()>();
  1202. doShowNativeNotification({
  1203. .peer = item->history()->peer,
  1204. .topicRootId = item->topicRootId(),
  1205. .itemId = item->id,
  1206. .title = scheduled ? WrapFromScheduled(fullTitle) : fullTitle,
  1207. .subtitle = subtitle,
  1208. .message = text,
  1209. .sound = sound,
  1210. .options = options,
  1211. }, userpicView);
  1212. }
  1213. bool NativeManager::forceHideDetails() const {
  1214. return Core::App().screenIsLocked();
  1215. }
  1216. System::~System() = default;
  1217. QString WrapFromScheduled(const QString &text) {
  1218. return QString::fromUtf8("\xF0\x9F\x93\x85 ") + text;
  1219. }
  1220. } // namespace Notifications
  1221. } // namespace Window