626 lines
18 KiB
Plaintext
626 lines
18 KiB
Plaintext
/*
|
|
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 (_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
|