notifications_manager_mac.mm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  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/mac/notifications_manager_mac.h"
  8. #include "core/application.h"
  9. #include "core/core_settings.h"
  10. #include "base/platform/base_platform_info.h"
  11. #include "platform/platform_specific.h"
  12. #include "base/platform/mac/base_utilities_mac.h"
  13. #include "base/random.h"
  14. #include "data/data_forum_topic.h"
  15. #include "history/history.h"
  16. #include "history/history_item.h"
  17. #include "ui/empty_userpic.h"
  18. #include "main/main_session.h"
  19. #include "mainwindow.h"
  20. #include "window/notifications_utilities.h"
  21. #include "styles/style_window.h"
  22. #include <thread>
  23. #include <Cocoa/Cocoa.h>
  24. namespace {
  25. constexpr auto kQuerySettingsEachMs = crl::time(1000);
  26. crl::time LastSettingsQueryMs/* = 0*/;
  27. bool DoNotDisturbEnabled/* = false*/;
  28. [[nodiscard]] bool ShouldQuerySettings() {
  29. const auto now = crl::now();
  30. if (LastSettingsQueryMs > 0 && now <= LastSettingsQueryMs + kQuerySettingsEachMs) {
  31. return false;
  32. }
  33. LastSettingsQueryMs = now;
  34. return true;
  35. }
  36. [[nodiscard]] QString LibraryPath() {
  37. static const auto result = [] {
  38. NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
  39. return url
  40. ? QString::fromUtf8([[url path] fileSystemRepresentation])
  41. : QString();
  42. }();
  43. return result;
  44. }
  45. void queryDoNotDisturbState() {
  46. if (!ShouldQuerySettings()) {
  47. return;
  48. }
  49. Boolean isKeyValid;
  50. const auto doNotDisturb = CFPreferencesGetAppBooleanValue(
  51. CFSTR("doNotDisturb"),
  52. CFSTR("com.apple.notificationcenterui"),
  53. &isKeyValid);
  54. DoNotDisturbEnabled = isKeyValid
  55. ? doNotDisturb
  56. : false;
  57. }
  58. using Manager = Platform::Notifications::Manager;
  59. } // namespace
  60. @interface NotificationDelegate : NSObject<NSUserNotificationCenterDelegate> {
  61. }
  62. - (id) initWithManager:(base::weak_ptr<Manager>)manager managerId:(uint64)managerId;
  63. - (void) userNotificationCenter:(NSUserNotificationCenter*)center didActivateNotification:(NSUserNotification*)notification;
  64. - (BOOL) userNotificationCenter:(NSUserNotificationCenter*)center shouldPresentNotification:(NSUserNotification*)notification;
  65. @end // @interface NotificationDelegate
  66. @implementation NotificationDelegate {
  67. base::weak_ptr<Manager> _manager;
  68. uint64 _managerId;
  69. }
  70. - (id) initWithManager:(base::weak_ptr<Manager>)manager managerId:(uint64)managerId {
  71. if (self = [super init]) {
  72. _manager = manager;
  73. _managerId = managerId;
  74. }
  75. return self;
  76. }
  77. - (void) userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification {
  78. NSDictionary *notificationUserInfo = [notification userInfo];
  79. NSNumber *managerIdObject = [notificationUserInfo objectForKey:@"manager"];
  80. auto notificationManagerId = managerIdObject ? [managerIdObject unsignedLongLongValue] : 0ULL;
  81. DEBUG_LOG(("Received notification with instance %1, mine: %2").arg(notificationManagerId).arg(_managerId));
  82. if (notificationManagerId != _managerId) { // other app instance notification
  83. crl::on_main([] {
  84. // Usually we show and activate main window when the application
  85. // is activated (receives applicationDidBecomeActive: notification).
  86. //
  87. // This is used for window show in Cmd+Tab switching to the application.
  88. //
  89. // But when a notification arrives sometimes macOS still activates the app
  90. // and we receive applicationDidBecomeActive: notification even if the
  91. // notification was sent by another instance of the application. In that case
  92. // we set a flag for a couple of seconds to ignore this app activation.
  93. objc_ignoreApplicationActivationRightNow();
  94. });
  95. return;
  96. }
  97. NSNumber *sessionObject = [notificationUserInfo objectForKey:@"session"];
  98. const auto notificationSessionId = sessionObject ? [sessionObject unsignedLongLongValue] : 0;
  99. if (!notificationSessionId) {
  100. LOG(("App Error: A notification with unknown session was received"));
  101. return;
  102. }
  103. NSNumber *peerObject = [notificationUserInfo objectForKey:@"peer"];
  104. const auto notificationPeerId = peerObject ? [peerObject unsignedLongLongValue] : 0ULL;
  105. if (!notificationPeerId) {
  106. LOG(("App Error: A notification with unknown peer was received"));
  107. return;
  108. }
  109. NSNumber *topicObject = [notificationUserInfo objectForKey:@"topic"];
  110. if (!topicObject) {
  111. LOG(("App Error: A notification with unknown topic was received"));
  112. return;
  113. }
  114. const auto notificationTopicRootId = [topicObject longLongValue];
  115. NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"];
  116. const auto notificationMsgId = msgObject ? [msgObject longLongValue] : 0LL;
  117. const auto my = Window::Notifications::Manager::NotificationId{
  118. .contextId = Manager::ContextId{
  119. .sessionId = notificationSessionId,
  120. .peerId = PeerId(notificationPeerId),
  121. .topicRootId = MsgId(notificationTopicRootId),
  122. },
  123. .msgId = notificationMsgId,
  124. };
  125. if (notification.activationType == NSUserNotificationActivationTypeReplied) {
  126. const auto notificationReply = QString::fromUtf8([[[notification response] string] UTF8String]);
  127. const auto manager = _manager;
  128. crl::on_main(manager, [=] {
  129. manager->notificationReplied(my, { notificationReply, {} });
  130. });
  131. } else if (notification.activationType == NSUserNotificationActivationTypeContentsClicked) {
  132. const auto manager = _manager;
  133. crl::on_main(manager, [=] {
  134. manager->notificationActivated(my);
  135. });
  136. }
  137. [center removeDeliveredNotification: notification];
  138. }
  139. - (BOOL) userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification {
  140. return YES;
  141. }
  142. @end // @implementation NotificationDelegate
  143. namespace Platform {
  144. namespace Notifications {
  145. bool SkipToastForCustom() {
  146. return false;
  147. }
  148. void MaybePlaySoundForCustom(Fn<void()> playSound) {
  149. playSound();
  150. }
  151. void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
  152. flashBounce();
  153. }
  154. bool WaitForInputForCustom() {
  155. return true;
  156. }
  157. bool Supported() {
  158. return true;
  159. }
  160. bool Enforced() {
  161. return Supported();
  162. }
  163. bool ByDefault() {
  164. return Supported();
  165. }
  166. void Create(Window::Notifications::System *system) {
  167. system->setManager([=] { return std::make_unique<Manager>(system); });
  168. }
  169. class Manager::Private : public QObject {
  170. public:
  171. Private(Manager *manager);
  172. void showNotification(
  173. NotificationInfo &&info,
  174. Ui::PeerUserpicView &userpicView);
  175. void clearAll();
  176. void clearFromItem(not_null<HistoryItem*> item);
  177. void clearFromTopic(not_null<Data::ForumTopic*> topic);
  178. void clearFromHistory(not_null<History*> history);
  179. void clearFromSession(not_null<Main::Session*> session);
  180. void updateDelegate();
  181. ~Private();
  182. private:
  183. template <typename Task>
  184. void putClearTask(Task task);
  185. void clearingThreadLoop();
  186. const uint64 _managerId = 0;
  187. QString _managerIdString;
  188. NotificationDelegate *_delegate = nullptr;
  189. std::thread _clearingThread;
  190. std::mutex _clearingMutex;
  191. std::condition_variable _clearingCondition;
  192. struct ClearFromItem {
  193. NotificationId id;
  194. };
  195. struct ClearFromTopic {
  196. ContextId contextId;
  197. };
  198. struct ClearFromHistory {
  199. ContextId partialContextId;
  200. };
  201. struct ClearFromSession {
  202. uint64 sessionId = 0;
  203. };
  204. struct ClearAll {
  205. };
  206. struct ClearFinish {
  207. };
  208. using ClearTask = std::variant<
  209. ClearFromItem,
  210. ClearFromTopic,
  211. ClearFromHistory,
  212. ClearFromSession,
  213. ClearAll,
  214. ClearFinish>;
  215. std::vector<ClearTask> _clearingTasks;
  216. Media::Audio::LocalDiskCache _sounds;
  217. rpl::lifetime _lifetime;
  218. };
  219. [[nodiscard]] QString ResolveSoundsFolder() {
  220. NSArray *paths = NSSearchPathForDirectoriesInDomains(
  221. NSLibraryDirectory,
  222. NSUserDomainMask,
  223. YES);
  224. NSString *library = [paths firstObject];
  225. NSString *sounds = [library stringByAppendingPathComponent : @"Sounds"];
  226. return NS2QString(sounds);
  227. }
  228. Manager::Private::Private(Manager *manager)
  229. : _managerId(base::RandomValue<uint64>())
  230. , _managerIdString(QString::number(_managerId))
  231. , _delegate([[NotificationDelegate alloc] initWithManager:manager managerId:_managerId])
  232. , _sounds(ResolveSoundsFolder()) {
  233. Core::App().settings().workModeValue(
  234. ) | rpl::start_with_next([=](Core::Settings::WorkMode mode) {
  235. // We need to update the delegate _after_ the tray icon change was done in Qt.
  236. // Because Qt resets the delegate.
  237. crl::on_main(this, [=] {
  238. updateDelegate();
  239. });
  240. }, _lifetime);
  241. }
  242. void Manager::Private::showNotification(
  243. NotificationInfo &&info,
  244. Ui::PeerUserpicView &userpicView) {
  245. @autoreleasepool {
  246. const auto peer = info.peer;
  247. NSUserNotification *notification = [[[NSUserNotification alloc] init] autorelease];
  248. if ([notification respondsToSelector:@selector(setIdentifier:)]) {
  249. auto identifier = _managerIdString
  250. + '_'
  251. + QString::number(peer->id.value)
  252. + '_'
  253. + QString::number(info.itemId.bare);
  254. auto identifierValue = Q2NSString(identifier);
  255. [notification setIdentifier:identifierValue];
  256. }
  257. [notification setUserInfo:
  258. [NSDictionary dictionaryWithObjectsAndKeys:
  259. [NSNumber numberWithUnsignedLongLong:peer->session().uniqueId()],
  260. @"session",
  261. [NSNumber numberWithUnsignedLongLong:peer->id.value],
  262. @"peer",
  263. [NSNumber numberWithLongLong:info.topicRootId.bare],
  264. @"topic",
  265. [NSNumber numberWithLongLong:info.itemId.bare],
  266. @"msgid",
  267. [NSNumber numberWithUnsignedLongLong:_managerId],
  268. @"manager",
  269. nil]];
  270. [notification setTitle:Q2NSString(info.title)];
  271. [notification setSubtitle:Q2NSString(info.subtitle)];
  272. [notification setInformativeText:Q2NSString(info.message)];
  273. if (!info.options.hideNameAndPhoto
  274. && [notification respondsToSelector:@selector(setContentImage:)]) {
  275. NSImage *img = Q2NSImage(
  276. Window::Notifications::GenerateUserpic(peer, userpicView));
  277. [notification setContentImage:img];
  278. }
  279. if (!info.options.hideReplyButton
  280. && [notification respondsToSelector:@selector(setHasReplyButton:)]) {
  281. [notification setHasReplyButton:YES];
  282. }
  283. const auto sound = info.sound ? info.sound() : Media::Audio::LocalSound();
  284. if (sound) {
  285. [notification setSoundName:Q2NSString(_sounds.name(sound))];
  286. } else {
  287. [notification setSoundName:nil];
  288. }
  289. NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
  290. [center deliverNotification:notification];
  291. }
  292. }
  293. void Manager::Private::clearingThreadLoop() {
  294. auto finished = false;
  295. while (!finished) {
  296. auto clearAll = false;
  297. auto clearFromItems = base::flat_set<NotificationId>();
  298. auto clearFromTopics = base::flat_set<ContextId>();
  299. auto clearFromHistories = base::flat_set<ContextId>();
  300. auto clearFromSessions = base::flat_set<uint64>();
  301. {
  302. std::unique_lock<std::mutex> lock(_clearingMutex);
  303. while (_clearingTasks.empty()) {
  304. _clearingCondition.wait(lock);
  305. }
  306. for (auto &task : _clearingTasks) {
  307. v::match(task, [&](ClearFinish) {
  308. finished = true;
  309. clearAll = true;
  310. }, [&](ClearAll) {
  311. clearAll = true;
  312. }, [&](const ClearFromItem &value) {
  313. clearFromItems.emplace(value.id);
  314. }, [&](const ClearFromTopic &value) {
  315. clearFromTopics.emplace(value.contextId);
  316. }, [&](const ClearFromHistory &value) {
  317. clearFromHistories.emplace(value.partialContextId);
  318. }, [&](const ClearFromSession &value) {
  319. clearFromSessions.emplace(value.sessionId);
  320. });
  321. }
  322. _clearingTasks.clear();
  323. }
  324. @autoreleasepool {
  325. auto clearBySpecial = [&](NSDictionary *notificationUserInfo) {
  326. NSNumber *sessionObject = [notificationUserInfo objectForKey:@"session"];
  327. const auto notificationSessionId = sessionObject ? [sessionObject unsignedLongLongValue] : 0;
  328. if (!notificationSessionId) {
  329. return true;
  330. }
  331. NSNumber *peerObject = [notificationUserInfo objectForKey:@"peer"];
  332. const auto notificationPeerId = peerObject ? [peerObject unsignedLongLongValue] : 0;
  333. if (!notificationPeerId) {
  334. return true;
  335. }
  336. NSNumber *topicObject = [notificationUserInfo objectForKey:@"topic"];
  337. if (!topicObject) {
  338. return true;
  339. }
  340. const auto notificationTopicRootId = [topicObject longLongValue];
  341. NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"];
  342. const auto msgId = msgObject ? [msgObject longLongValue] : 0LL;
  343. const auto partialContextId = ContextId{
  344. .sessionId = notificationSessionId,
  345. .peerId = PeerId(notificationPeerId),
  346. };
  347. const auto contextId = ContextId{
  348. .sessionId = notificationSessionId,
  349. .peerId = PeerId(notificationPeerId),
  350. .topicRootId = MsgId(notificationTopicRootId),
  351. };
  352. const auto id = NotificationId{ contextId, MsgId(msgId) };
  353. return clearFromSessions.contains(notificationSessionId)
  354. || clearFromHistories.contains(partialContextId)
  355. || clearFromTopics.contains(contextId)
  356. || (msgId && clearFromItems.contains(id));
  357. };
  358. NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
  359. NSArray *notificationsList = [center deliveredNotifications];
  360. for (id notification in notificationsList) {
  361. NSDictionary *notificationUserInfo = [notification userInfo];
  362. NSNumber *managerIdObject = [notificationUserInfo objectForKey:@"manager"];
  363. auto notificationManagerId = managerIdObject ? [managerIdObject unsignedLongLongValue] : 0ULL;
  364. if (notificationManagerId == _managerId) {
  365. if (clearAll || clearBySpecial(notificationUserInfo)) {
  366. [center removeDeliveredNotification:notification];
  367. }
  368. }
  369. }
  370. }
  371. }
  372. }
  373. template <typename Task>
  374. void Manager::Private::putClearTask(Task task) {
  375. if (!_clearingThread.joinable()) {
  376. _clearingThread = std::thread([this] { clearingThreadLoop(); });
  377. }
  378. std::unique_lock<std::mutex> lock(_clearingMutex);
  379. _clearingTasks.push_back(task);
  380. _clearingCondition.notify_one();
  381. }
  382. void Manager::Private::clearAll() {
  383. putClearTask(ClearAll());
  384. }
  385. void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
  386. putClearTask(ClearFromItem{ ContextId{
  387. .sessionId = item->history()->session().uniqueId(),
  388. .peerId = item->history()->peer->id,
  389. .topicRootId = item->topicRootId(),
  390. }, item->id });
  391. }
  392. void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
  393. putClearTask(ClearFromTopic{ ContextId{
  394. .sessionId = topic->session().uniqueId(),
  395. .peerId = topic->history()->peer->id,
  396. .topicRootId = topic->rootId(),
  397. } });
  398. }
  399. void Manager::Private::clearFromHistory(not_null<History*> history) {
  400. putClearTask(ClearFromHistory{ ContextId{
  401. .sessionId = history->session().uniqueId(),
  402. .peerId = history->peer->id,
  403. } });
  404. }
  405. void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
  406. putClearTask(ClearFromSession{ session->uniqueId() });
  407. }
  408. void Manager::Private::updateDelegate() {
  409. NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
  410. [center setDelegate:_delegate];
  411. }
  412. Manager::Private::~Private() {
  413. if (_clearingThread.joinable()) {
  414. putClearTask(ClearFinish());
  415. _clearingThread.join();
  416. }
  417. NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
  418. [center setDelegate:nil];
  419. [_delegate release];
  420. }
  421. Manager::Manager(Window::Notifications::System *system) : NativeManager(system)
  422. , _private(std::make_unique<Private>(this)) {
  423. }
  424. Manager::~Manager() = default;
  425. void Manager::doShowNativeNotification(
  426. NotificationInfo &&info,
  427. Ui::PeerUserpicView &userpicView) {
  428. _private->showNotification(std::move(info), userpicView);
  429. }
  430. void Manager::doClearAllFast() {
  431. _private->clearAll();
  432. }
  433. void Manager::doClearFromItem(not_null<HistoryItem*> item) {
  434. _private->clearFromItem(item);
  435. }
  436. void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {
  437. _private->clearFromTopic(topic);
  438. }
  439. void Manager::doClearFromHistory(not_null<History*> history) {
  440. _private->clearFromHistory(history);
  441. }
  442. void Manager::doClearFromSession(not_null<Main::Session*> session) {
  443. _private->clearFromSession(session);
  444. }
  445. QString Manager::accountNameSeparator() {
  446. return QString::fromUtf8(" \xE2\x86\x92 ");
  447. }
  448. bool Manager::doSkipToast() const {
  449. return false;
  450. }
  451. void Manager::doMaybePlaySound(Fn<void()> playSound) {
  452. // Play through native notification system if toasts are enabled.
  453. if (!Core::App().settings().desktopNotify()) {
  454. playSound();
  455. }
  456. }
  457. void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
  458. flashBounce();
  459. }
  460. } // namespace Notifications
  461. } // namespace Platform