/* 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 "base/platform/base_platform_info.h" #include "platform/platform_specific.h" #include "base/platform/mac/base_utilities_mac.h" #include "history/history.h" #include "ui/empty_userpic.h" #include "main/main_session.h" #include "mainwindow.h" #include "facades.h" #include "styles/style_window.h" #include #include namespace { static constexpr auto kQuerySettingsEachMs = 1000; auto DoNotDisturbEnabled = false; auto LastSettingsQueryMs = 0; void queryDoNotDisturbState() { auto ms = crl::now(); if (LastSettingsQueryMs > 0 && ms <= LastSettingsQueryMs + kQuerySettingsEachMs) { return; } LastSettingsQueryMs = ms; Boolean isKeyValid; const auto doNotDisturb = CFPreferencesGetAppBooleanValue( CFSTR("doNotDisturb"), CFSTR("com.apple.notificationcenterui"), &isKeyValid); DoNotDisturbEnabled = isKeyValid ? doNotDisturb : false; } using Manager = Platform::Notifications::Manager; } // namespace NSImage *qt_mac_create_nsimage(const QPixmap &pm); @interface NotificationDelegate : NSObject { } - (id) initWithManager:(base::weak_ptr)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; uint64 _managerId; } - (id) initWithManager:(base::weak_ptr)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 *msgObject = [notificationUserInfo objectForKey:@"msgid"]; const auto notificationMsgId = msgObject ? [msgObject intValue] : 0; const auto my = Window::Notifications::Manager::NotificationId{ .full = Manager::FullPeer{ .sessionId = notificationSessionId, .peerId = notificationPeerId }, .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 SkipAudio() { queryDoNotDisturbState(); return DoNotDisturbEnabled; } bool SkipToast() { if (Supported()) { // Do not skip native notifications because of Do not disturb. // They respect this setting anyway. return false; } queryDoNotDisturbState(); return DoNotDisturbEnabled; } bool SkipFlashBounce() { return SkipAudio(); } bool Supported() { return Platform::IsMac10_8OrGreater(); } std::unique_ptr Create(Window::Notifications::System *system) { if (Supported()) { return std::make_unique(system); } return nullptr; } class Manager::Private : public QObject, private base::Subscriber { public: Private(Manager *manager); void showNotification( not_null peer, std::shared_ptr &userpicView, MsgId msgId, const QString &title, const QString &subtitle, const QString &msg, bool hideNameAndPhoto, bool hideReplyButton); void clearAll(); void clearFromHistory(not_null history); void clearFromSession(not_null session); void updateDelegate(); ~Private(); private: template 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 ClearFromHistory { FullPeer fullPeer; }; struct ClearFromSession { uint64 sessionId = 0; }; struct ClearAll { }; struct ClearFinish { }; using ClearTask = std::variant< ClearFromHistory, ClearFromSession, ClearAll, ClearFinish>; std::vector _clearingTasks; }; Manager::Private::Private(Manager *manager) : _managerId(rand_value()) , _managerIdString(QString::number(_managerId)) , _delegate([[NotificationDelegate alloc] initWithManager:manager managerId:_managerId]) { updateDelegate(); subscribe(Global::RefWorkMode(), [this](DBIWorkMode 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(); }); }); } void Manager::Private::showNotification( not_null peer, std::shared_ptr &userpicView, MsgId msgId, const QString &title, const QString &subtitle, const QString &msg, bool hideNameAndPhoto, bool hideReplyButton) { @autoreleasepool { NSUserNotification *notification = [[[NSUserNotification alloc] init] autorelease]; if ([notification respondsToSelector:@selector(setIdentifier:)]) { auto identifier = _managerIdString + '_' + QString::number(peer->id) + '_' + QString::number(msgId); auto identifierValue = Q2NSString(identifier); [notification setIdentifier:identifierValue]; } [notification setUserInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithUnsignedLongLong:peer->session().uniqueId()],@"session",[NSNumber numberWithUnsignedLongLong:peer->id],@"peer",[NSNumber numberWithInt:msgId],@"msgid",[NSNumber numberWithUnsignedLongLong:_managerId],@"manager",nil]]; [notification setTitle:Q2NSString(title)]; [notification setSubtitle:Q2NSString(subtitle)]; [notification setInformativeText:Q2NSString(msg)]; if (!hideNameAndPhoto && [notification respondsToSelector:@selector(setContentImage:)]) { auto userpic = peer->isSelf() ? Ui::EmptyUserpic::GenerateSavedMessages(st::notifyMacPhotoSize) : peer->isRepliesChat() ? Ui::EmptyUserpic::GenerateRepliesMessages(st::notifyMacPhotoSize) : peer->genUserpic(userpicView, st::notifyMacPhotoSize); NSImage *img = [qt_mac_create_nsimage(userpic) autorelease]; [notification setContentImage:img]; } if (!hideReplyButton && [notification respondsToSelector:@selector(setHasReplyButton:)]) { [notification setHasReplyButton:YES]; } [notification setSoundName:nil]; NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; [center deliverNotification:notification]; } } void Manager::Private::clearingThreadLoop() { auto finished = false; while (!finished) { auto clearAll = false; auto clearFromPeers = base::flat_set(); auto clearFromSessions = base::flat_set(); { std::unique_lock lock(_clearingMutex); while (_clearingTasks.empty()) { _clearingCondition.wait(lock); } for (auto &task : _clearingTasks) { v::match(task, [&](ClearFinish) { finished = true; clearAll = true; }, [&](ClearAll) { clearAll = true; }, [&](const ClearFromHistory &value) { clearFromPeers.emplace(value.fullPeer); }, [&](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; } if (NSNumber *peerObject = [notificationUserInfo objectForKey:@"peer"]) { const auto notificationPeerId = [peerObject unsignedLongLongValue]; if (notificationPeerId) { return clearFromSessions.contains(notificationSessionId) || clearFromPeers.contains(FullPeer{ .sessionId = notificationSessionId, .peerId = notificationPeerId }); } } return true; }; 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 void Manager::Private::putClearTask(Task task) { if (!_clearingThread.joinable()) { _clearingThread = std::thread([this] { clearingThreadLoop(); }); } std::unique_lock lock(_clearingMutex); _clearingTasks.push_back(task); _clearingCondition.notify_one(); } void Manager::Private::clearAll() { putClearTask(ClearAll()); } void Manager::Private::clearFromHistory(not_null history) { putClearTask(ClearFromHistory { FullPeer{ .sessionId = history->session().uniqueId(), .peerId = history->peer->id } }); } void Manager::Private::clearFromSession(not_null 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(this)) { } Manager::~Manager() = default; void Manager::doShowNativeNotification( not_null peer, std::shared_ptr &userpicView, MsgId msgId, const QString &title, const QString &subtitle, const QString &msg, bool hideNameAndPhoto, bool hideReplyButton) { _private->showNotification( peer, userpicView, msgId, title, subtitle, msg, hideNameAndPhoto, hideReplyButton); } void Manager::doClearAllFast() { _private->clearAll(); } void Manager::doClearFromHistory(not_null history) { _private->clearFromHistory(history); } void Manager::doClearFromSession(not_null session) { _private->clearFromSession(session); } QString Manager::accountNameSeparator() { return QString::fromUtf8(" \xE2\x86\x92 "); } } // namespace Notifications } // namespace Platform