| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- /*
- This file is part of Telegram Desktop,
- the official desktop application for the Telegram messaging service.
- For license and copyright information please follow this link:
- https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
- */
- #include "platform/mac/notifications_manager_mac.h"
- #include "core/application.h"
- #include "core/core_settings.h"
- #include "base/platform/base_platform_info.h"
- #include "platform/platform_specific.h"
- #include "base/platform/mac/base_utilities_mac.h"
- #include "base/random.h"
- #include "data/data_forum_topic.h"
- #include "history/history.h"
- #include "history/history_item.h"
- #include "ui/empty_userpic.h"
- #include "main/main_session.h"
- #include "mainwindow.h"
- #include "window/notifications_utilities.h"
- #include "styles/style_window.h"
- #include <thread>
- #include <Cocoa/Cocoa.h>
- namespace {
- constexpr auto kQuerySettingsEachMs = crl::time(1000);
- crl::time LastSettingsQueryMs/* = 0*/;
- bool DoNotDisturbEnabled/* = false*/;
- [[nodiscard]] bool ShouldQuerySettings() {
- const auto now = crl::now();
- if (LastSettingsQueryMs > 0 && now <= LastSettingsQueryMs + kQuerySettingsEachMs) {
- return false;
- }
- LastSettingsQueryMs = now;
- return true;
- }
- [[nodiscard]] QString LibraryPath() {
- static const auto result = [] {
- NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
- return url
- ? QString::fromUtf8([[url path] fileSystemRepresentation])
- : QString();
- }();
- return result;
- }
- void queryDoNotDisturbState() {
- if (!ShouldQuerySettings()) {
- return;
- }
- Boolean isKeyValid;
- const auto doNotDisturb = CFPreferencesGetAppBooleanValue(
- CFSTR("doNotDisturb"),
- CFSTR("com.apple.notificationcenterui"),
- &isKeyValid);
- DoNotDisturbEnabled = isKeyValid
- ? doNotDisturb
- : false;
- }
- using Manager = Platform::Notifications::Manager;
- } // namespace
- @interface NotificationDelegate : NSObject<NSUserNotificationCenterDelegate> {
- }
- - (id) initWithManager:(base::weak_ptr<Manager>)manager managerId:(uint64)managerId;
- - (void) userNotificationCenter:(NSUserNotificationCenter*)center didActivateNotification:(NSUserNotification*)notification;
- - (BOOL) userNotificationCenter:(NSUserNotificationCenter*)center shouldPresentNotification:(NSUserNotification*)notification;
- @end // @interface NotificationDelegate
- @implementation NotificationDelegate {
- base::weak_ptr<Manager> _manager;
- uint64 _managerId;
- }
- - (id) initWithManager:(base::weak_ptr<Manager>)manager managerId:(uint64)managerId {
- if (self = [super init]) {
- _manager = manager;
- _managerId = managerId;
- }
- return self;
- }
- - (void) userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification {
- NSDictionary *notificationUserInfo = [notification userInfo];
- NSNumber *managerIdObject = [notificationUserInfo objectForKey:@"manager"];
- auto notificationManagerId = managerIdObject ? [managerIdObject unsignedLongLongValue] : 0ULL;
- DEBUG_LOG(("Received notification with instance %1, mine: %2").arg(notificationManagerId).arg(_managerId));
- if (notificationManagerId != _managerId) { // other app instance notification
- crl::on_main([] {
- // Usually we show and activate main window when the application
- // is activated (receives applicationDidBecomeActive: notification).
- //
- // This is used for window show in Cmd+Tab switching to the application.
- //
- // But when a notification arrives sometimes macOS still activates the app
- // and we receive applicationDidBecomeActive: notification even if the
- // notification was sent by another instance of the application. In that case
- // we set a flag for a couple of seconds to ignore this app activation.
- objc_ignoreApplicationActivationRightNow();
- });
- return;
- }
- NSNumber *sessionObject = [notificationUserInfo objectForKey:@"session"];
- const auto notificationSessionId = sessionObject ? [sessionObject unsignedLongLongValue] : 0;
- if (!notificationSessionId) {
- LOG(("App Error: A notification with unknown session was received"));
- return;
- }
- NSNumber *peerObject = [notificationUserInfo objectForKey:@"peer"];
- const auto notificationPeerId = peerObject ? [peerObject unsignedLongLongValue] : 0ULL;
- if (!notificationPeerId) {
- LOG(("App Error: A notification with unknown peer was received"));
- return;
- }
- NSNumber *topicObject = [notificationUserInfo objectForKey:@"topic"];
- if (!topicObject) {
- LOG(("App Error: A notification with unknown topic was received"));
- return;
- }
- const auto notificationTopicRootId = [topicObject longLongValue];
- NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"];
- const auto notificationMsgId = msgObject ? [msgObject longLongValue] : 0LL;
- const auto my = Window::Notifications::Manager::NotificationId{
- .contextId = Manager::ContextId{
- .sessionId = notificationSessionId,
- .peerId = PeerId(notificationPeerId),
- .topicRootId = MsgId(notificationTopicRootId),
- },
- .msgId = notificationMsgId,
- };
- if (notification.activationType == NSUserNotificationActivationTypeReplied) {
- const auto notificationReply = QString::fromUtf8([[[notification response] string] UTF8String]);
- const auto manager = _manager;
- crl::on_main(manager, [=] {
- manager->notificationReplied(my, { notificationReply, {} });
- });
- } else if (notification.activationType == NSUserNotificationActivationTypeContentsClicked) {
- const auto manager = _manager;
- crl::on_main(manager, [=] {
- manager->notificationActivated(my);
- });
- }
- [center removeDeliveredNotification: notification];
- }
- - (BOOL) userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification {
- return YES;
- }
- @end // @implementation NotificationDelegate
- namespace Platform {
- namespace Notifications {
- bool SkipToastForCustom() {
- return false;
- }
- void MaybePlaySoundForCustom(Fn<void()> playSound) {
- playSound();
- }
- void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
- flashBounce();
- }
- bool WaitForInputForCustom() {
- return true;
- }
- bool Supported() {
- return true;
- }
- bool Enforced() {
- return Supported();
- }
- bool ByDefault() {
- return Supported();
- }
- void Create(Window::Notifications::System *system) {
- system->setManager([=] { return std::make_unique<Manager>(system); });
- }
- class Manager::Private : public QObject {
- public:
- Private(Manager *manager);
- void showNotification(
- NotificationInfo &&info,
- Ui::PeerUserpicView &userpicView);
- void clearAll();
- void clearFromItem(not_null<HistoryItem*> item);
- void clearFromTopic(not_null<Data::ForumTopic*> topic);
- void clearFromHistory(not_null<History*> history);
- void clearFromSession(not_null<Main::Session*> session);
- void updateDelegate();
- ~Private();
- private:
- template <typename Task>
- void putClearTask(Task task);
- void clearingThreadLoop();
- const uint64 _managerId = 0;
- QString _managerIdString;
- NotificationDelegate *_delegate = nullptr;
- std::thread _clearingThread;
- std::mutex _clearingMutex;
- std::condition_variable _clearingCondition;
- struct ClearFromItem {
- NotificationId id;
- };
- struct ClearFromTopic {
- ContextId contextId;
- };
- struct ClearFromHistory {
- ContextId partialContextId;
- };
- struct ClearFromSession {
- uint64 sessionId = 0;
- };
- struct ClearAll {
- };
- struct ClearFinish {
- };
- using ClearTask = std::variant<
- ClearFromItem,
- ClearFromTopic,
- ClearFromHistory,
- ClearFromSession,
- ClearAll,
- ClearFinish>;
- std::vector<ClearTask> _clearingTasks;
- Media::Audio::LocalDiskCache _sounds;
- rpl::lifetime _lifetime;
- };
- [[nodiscard]] QString ResolveSoundsFolder() {
- NSArray *paths = NSSearchPathForDirectoriesInDomains(
- NSLibraryDirectory,
- NSUserDomainMask,
- YES);
- NSString *library = [paths firstObject];
- NSString *sounds = [library stringByAppendingPathComponent : @"Sounds"];
- return NS2QString(sounds);
- }
- Manager::Private::Private(Manager *manager)
- : _managerId(base::RandomValue<uint64>())
- , _managerIdString(QString::number(_managerId))
- , _delegate([[NotificationDelegate alloc] initWithManager:manager managerId:_managerId])
- , _sounds(ResolveSoundsFolder()) {
- Core::App().settings().workModeValue(
- ) | rpl::start_with_next([=](Core::Settings::WorkMode mode) {
- // We need to update the delegate _after_ the tray icon change was done in Qt.
- // Because Qt resets the delegate.
- crl::on_main(this, [=] {
- updateDelegate();
- });
- }, _lifetime);
- }
- void Manager::Private::showNotification(
- NotificationInfo &&info,
- Ui::PeerUserpicView &userpicView) {
- @autoreleasepool {
- const auto peer = info.peer;
- NSUserNotification *notification = [[[NSUserNotification alloc] init] autorelease];
- if ([notification respondsToSelector:@selector(setIdentifier:)]) {
- auto identifier = _managerIdString
- + '_'
- + QString::number(peer->id.value)
- + '_'
- + QString::number(info.itemId.bare);
- auto identifierValue = Q2NSString(identifier);
- [notification setIdentifier:identifierValue];
- }
- [notification setUserInfo:
- [NSDictionary dictionaryWithObjectsAndKeys:
- [NSNumber numberWithUnsignedLongLong:peer->session().uniqueId()],
- @"session",
- [NSNumber numberWithUnsignedLongLong:peer->id.value],
- @"peer",
- [NSNumber numberWithLongLong:info.topicRootId.bare],
- @"topic",
- [NSNumber numberWithLongLong:info.itemId.bare],
- @"msgid",
- [NSNumber numberWithUnsignedLongLong:_managerId],
- @"manager",
- nil]];
- [notification setTitle:Q2NSString(info.title)];
- [notification setSubtitle:Q2NSString(info.subtitle)];
- [notification setInformativeText:Q2NSString(info.message)];
- if (!info.options.hideNameAndPhoto
- && [notification respondsToSelector:@selector(setContentImage:)]) {
- NSImage *img = Q2NSImage(
- Window::Notifications::GenerateUserpic(peer, userpicView));
- [notification setContentImage:img];
- }
- if (!info.options.hideReplyButton
- && [notification respondsToSelector:@selector(setHasReplyButton:)]) {
- [notification setHasReplyButton:YES];
- }
- const auto sound = info.sound ? info.sound() : Media::Audio::LocalSound();
- if (sound) {
- [notification setSoundName:Q2NSString(_sounds.name(sound))];
- } else {
- [notification setSoundName:nil];
- }
- NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
- [center deliverNotification:notification];
- }
- }
- void Manager::Private::clearingThreadLoop() {
- auto finished = false;
- while (!finished) {
- auto clearAll = false;
- auto clearFromItems = base::flat_set<NotificationId>();
- auto clearFromTopics = base::flat_set<ContextId>();
- auto clearFromHistories = base::flat_set<ContextId>();
- auto clearFromSessions = base::flat_set<uint64>();
- {
- std::unique_lock<std::mutex> lock(_clearingMutex);
- while (_clearingTasks.empty()) {
- _clearingCondition.wait(lock);
- }
- for (auto &task : _clearingTasks) {
- v::match(task, [&](ClearFinish) {
- finished = true;
- clearAll = true;
- }, [&](ClearAll) {
- clearAll = true;
- }, [&](const ClearFromItem &value) {
- clearFromItems.emplace(value.id);
- }, [&](const ClearFromTopic &value) {
- clearFromTopics.emplace(value.contextId);
- }, [&](const ClearFromHistory &value) {
- clearFromHistories.emplace(value.partialContextId);
- }, [&](const ClearFromSession &value) {
- clearFromSessions.emplace(value.sessionId);
- });
- }
- _clearingTasks.clear();
- }
- @autoreleasepool {
- auto clearBySpecial = [&](NSDictionary *notificationUserInfo) {
- NSNumber *sessionObject = [notificationUserInfo objectForKey:@"session"];
- const auto notificationSessionId = sessionObject ? [sessionObject unsignedLongLongValue] : 0;
- if (!notificationSessionId) {
- return true;
- }
- NSNumber *peerObject = [notificationUserInfo objectForKey:@"peer"];
- const auto notificationPeerId = peerObject ? [peerObject unsignedLongLongValue] : 0;
- if (!notificationPeerId) {
- return true;
- }
- NSNumber *topicObject = [notificationUserInfo objectForKey:@"topic"];
- if (!topicObject) {
- return true;
- }
- const auto notificationTopicRootId = [topicObject longLongValue];
- NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"];
- const auto msgId = msgObject ? [msgObject longLongValue] : 0LL;
- const auto partialContextId = ContextId{
- .sessionId = notificationSessionId,
- .peerId = PeerId(notificationPeerId),
- };
- const auto contextId = ContextId{
- .sessionId = notificationSessionId,
- .peerId = PeerId(notificationPeerId),
- .topicRootId = MsgId(notificationTopicRootId),
- };
- const auto id = NotificationId{ contextId, MsgId(msgId) };
- return clearFromSessions.contains(notificationSessionId)
- || clearFromHistories.contains(partialContextId)
- || clearFromTopics.contains(contextId)
- || (msgId && clearFromItems.contains(id));
- };
- NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
- NSArray *notificationsList = [center deliveredNotifications];
- for (id notification in notificationsList) {
- NSDictionary *notificationUserInfo = [notification userInfo];
- NSNumber *managerIdObject = [notificationUserInfo objectForKey:@"manager"];
- auto notificationManagerId = managerIdObject ? [managerIdObject unsignedLongLongValue] : 0ULL;
- if (notificationManagerId == _managerId) {
- if (clearAll || clearBySpecial(notificationUserInfo)) {
- [center removeDeliveredNotification:notification];
- }
- }
- }
- }
- }
- }
- template <typename Task>
- void Manager::Private::putClearTask(Task task) {
- if (!_clearingThread.joinable()) {
- _clearingThread = std::thread([this] { clearingThreadLoop(); });
- }
- std::unique_lock<std::mutex> lock(_clearingMutex);
- _clearingTasks.push_back(task);
- _clearingCondition.notify_one();
- }
- void Manager::Private::clearAll() {
- putClearTask(ClearAll());
- }
- void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
- putClearTask(ClearFromItem{ ContextId{
- .sessionId = item->history()->session().uniqueId(),
- .peerId = item->history()->peer->id,
- .topicRootId = item->topicRootId(),
- }, item->id });
- }
- void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
- putClearTask(ClearFromTopic{ ContextId{
- .sessionId = topic->session().uniqueId(),
- .peerId = topic->history()->peer->id,
- .topicRootId = topic->rootId(),
- } });
- }
- void Manager::Private::clearFromHistory(not_null<History*> history) {
- putClearTask(ClearFromHistory{ ContextId{
- .sessionId = history->session().uniqueId(),
- .peerId = history->peer->id,
- } });
- }
- void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
- putClearTask(ClearFromSession{ session->uniqueId() });
- }
- void Manager::Private::updateDelegate() {
- NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
- [center setDelegate:_delegate];
- }
- Manager::Private::~Private() {
- if (_clearingThread.joinable()) {
- putClearTask(ClearFinish());
- _clearingThread.join();
- }
- NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
- [center setDelegate:nil];
- [_delegate release];
- }
- Manager::Manager(Window::Notifications::System *system) : NativeManager(system)
- , _private(std::make_unique<Private>(this)) {
- }
- Manager::~Manager() = default;
- void Manager::doShowNativeNotification(
- NotificationInfo &&info,
- Ui::PeerUserpicView &userpicView) {
- _private->showNotification(std::move(info), userpicView);
- }
- void Manager::doClearAllFast() {
- _private->clearAll();
- }
- void Manager::doClearFromItem(not_null<HistoryItem*> item) {
- _private->clearFromItem(item);
- }
- void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {
- _private->clearFromTopic(topic);
- }
- void Manager::doClearFromHistory(not_null<History*> history) {
- _private->clearFromHistory(history);
- }
- void Manager::doClearFromSession(not_null<Main::Session*> session) {
- _private->clearFromSession(session);
- }
- QString Manager::accountNameSeparator() {
- return QString::fromUtf8(" \xE2\x86\x92 ");
- }
- bool Manager::doSkipToast() const {
- return false;
- }
- void Manager::doMaybePlaySound(Fn<void()> playSound) {
- // Play through native notification system if toasts are enabled.
- if (!Core::App().settings().desktopNotify()) {
- playSound();
- }
- }
- void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
- flashBounce();
- }
- } // namespace Notifications
- } // namespace Platform
|