tdesktop/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm
2024-05-24 14:08:14 +04:00

631 lines
18 KiB
Text

/*
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) {
if (Supported()) {
system->setManager(std::make_unique<Manager>(system));
} else {
system->setManager(nullptr);
}
}
class Manager::Private : public QObject {
public:
Private(Manager *manager);
void showNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options);
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();
void invokeIfNotFocused(Fn<void()> callback);
~Private();
private:
template <typename Task>
void putClearTask(Task task);
void clearingThreadLoop();
void checkFocusState();
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;
QProcess _dnd;
QProcess _focus;
std::vector<Fn<void()>> _focusedCallbacks;
bool _waitingDnd = false;
bool _waitingFocus = false;
bool _focused = false;
bool _processesInited = false;
rpl::lifetime _lifetime;
};
Manager::Private::Private(Manager *manager)
: _managerId(base::RandomValue<uint64>())
, _managerIdString(QString::number(_managerId))
, _delegate([[NotificationDelegate alloc] initWithManager:manager managerId:_managerId]) {
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(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) {
@autoreleasepool {
NSUserNotification *notification = [[[NSUserNotification alloc] init] autorelease];
if ([notification respondsToSelector:@selector(setIdentifier:)]) {
auto identifier = _managerIdString
+ '_'
+ QString::number(peer->id.value)
+ '_'
+ QString::number(msgId.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:topicRootId.bare],
@"topic",
[NSNumber numberWithLongLong:msgId.bare],
@"msgid",
[NSNumber numberWithUnsignedLongLong:_managerId],
@"manager",
nil]];
[notification setTitle:Q2NSString(title)];
[notification setSubtitle:Q2NSString(subtitle)];
[notification setInformativeText:Q2NSString(msg)];
if (!options.hideNameAndPhoto
&& [notification respondsToSelector:@selector(setContentImage:)]) {
NSImage *img = Q2NSImage(
Window::Notifications::GenerateUserpic(peer, userpicView));
[notification setContentImage:img];
}
if (!options.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 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];
}
void Manager::Private::invokeIfNotFocused(Fn<void()> callback) {
if (!Platform::IsMac11_0OrGreater()) {
queryDoNotDisturbState();
if (!DoNotDisturbEnabled) {
callback();
}
} else if (Platform::IsMacStoreBuild() || LibraryPath().isEmpty()) {
callback();
} else if (!_focusedCallbacks.empty()) {
_focusedCallbacks.push_back(std::move(callback));
} else if (!ShouldQuerySettings()) {
if (!_focused) {
callback();
}
} else {
if (!_processesInited) {
_processesInited = true;
QObject::connect(&_dnd, &QProcess::finished, [=] {
_waitingDnd = false;
checkFocusState();
});
QObject::connect(&_focus, &QProcess::finished, [=] {
_waitingFocus = false;
checkFocusState();
});
}
const auto start = [](QProcess &process, QString keys) {
auto arguments = QStringList()
<< "-extract"
<< keys
<< "raw"
<< "-o"
<< "-"
<< "--"
<< (LibraryPath() + "/Preferences/com.apple.controlcenter.plist");
DEBUG_LOG(("Focus Check: Started %1.").arg(u"plutil"_q + arguments.join(' ')));
process.start(u"plutil"_q, arguments);
};
_focusedCallbacks.push_back(std::move(callback));
_waitingFocus = _waitingDnd = true;
start(_focus, u"NSStatusItem Visible FocusModes"_q);
start(_dnd, u"NSStatusItem Visible DoNotDisturb"_q);
}
}
void Manager::Private::checkFocusState() {
if (_waitingFocus || _waitingDnd) {
return;
}
const auto istrue = [](QProcess &process) {
const auto output = process.readAllStandardOutput();
DEBUG_LOG(("Focus Check: %1").arg(output));
const auto result = (output.trimmed() == u"true"_q);
return result;
};
_focused = istrue(_focus) || istrue(_dnd);
auto callbacks = base::take(_focusedCallbacks);
if (!_focused) {
for (const auto &callback : callbacks) {
callback();
}
}
}
Manager::Private::~Private() {
if (_waitingDnd) {
_dnd.kill();
}
if (_waitingFocus) {
_focus.kill();
}
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(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) {
_private->showNotification(
peer,
topicRootId,
userpicView,
msgId,
title,
subtitle,
msg,
options);
}
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) {
_private->invokeIfNotFocused(std::move(playSound));
}
void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
_private->invokeIfNotFocused(std::move(flashBounce));
}
} // namespace Notifications
} // namespace Platform