diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index 286cd63138..87b1a7059b 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -715,23 +715,16 @@ void NotificationData::notificationReplied( } // namespace -bool SkipAudio() { - return Inhibited(); +bool SkipAudioForCustom() { + return false; } -bool SkipToast() { - // Do not skip native notifications because of Do not disturb. - // They respect this setting anyway. - if ((Core::App().settings().nativeNotifications() && Supported()) - || Enforced()) { - return false; - } - - return Inhibited(); +bool SkipToastForCustom() { + return false; } -bool SkipFlashBounce() { - return Inhibited(); +bool SkipFlashBounceForCustom() { + return false; } bool Supported() { @@ -1030,5 +1023,17 @@ void Manager::doClearFromSession(not_null session) { _private->clearFromSession(session); } +bool Manager::doSkipAudio() const { + return Inhibited(); +} + +bool Manager::doSkipToast() const { + return false; +} + +bool Manager::doSkipFlashBounce() const { + return Inhibited(); +} + } // namespace Notifications } // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h index 2e4d1f4a04..d4becaf7e7 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h @@ -34,6 +34,9 @@ protected: void doClearAllFast() override; void doClearFromHistory(not_null history) override; void doClearFromSession(not_null session) override; + bool doSkipAudio() const override; + bool doSkipToast() const override; + bool doSkipFlashBounce() const override; private: class Private; diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h index ee4d9e132a..a5f8de9820 100644 --- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h +++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h @@ -32,6 +32,9 @@ protected: void doClearFromHistory(not_null history) override; void doClearFromSession(not_null session) override; QString accountNameSeparator() override; + bool doSkipAudio() const override; + bool doSkipToast() const override; + bool doSkipFlashBounce() const override; private: class Private; diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm index 8affa37cb0..594d7228f0 100644 --- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm +++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm @@ -140,23 +140,16 @@ using Manager = Platform::Notifications::Manager; namespace Platform { namespace Notifications { -bool SkipAudio() { - queryDoNotDisturbState(); - return DoNotDisturbEnabled; +bool SkipAudioForCustom() { + return false; } -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 SkipToastForCustom() { + return false; } -bool SkipFlashBounce() { - return SkipAudio(); +bool SkipFlashBounceForCustom() { + return false; } bool Supported() { @@ -438,5 +431,18 @@ QString Manager::accountNameSeparator() { return QString::fromUtf8(" \xE2\x86\x92 "); } +bool Manager::doSkipAudio() const { + queryDoNotDisturbState(); + return DoNotDisturbEnabled; +} + +bool Manager::doSkipToast() const { + return false; +} + +bool Manager::doSkipFlashBounce() const { + return doSkipAudio(); +} + } // namespace Notifications } // namespace Platform diff --git a/Telegram/SourceFiles/platform/platform_notifications_manager.h b/Telegram/SourceFiles/platform/platform_notifications_manager.h index eb3efbddb3..8ab4826719 100644 --- a/Telegram/SourceFiles/platform/platform_notifications_manager.h +++ b/Telegram/SourceFiles/platform/platform_notifications_manager.h @@ -12,9 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Platform { namespace Notifications { -[[nodiscard]] bool SkipAudio(); -[[nodiscard]] bool SkipToast(); -[[nodiscard]] bool SkipFlashBounce(); +[[nodiscard]] bool SkipAudioForCustom(); +[[nodiscard]] bool SkipToastForCustom(); +[[nodiscard]] bool SkipFlashBounceForCustom(); [[nodiscard]] bool Supported(); [[nodiscard]] bool Enforced(); diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp index 25db6596a1..f9b3fd167e 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp @@ -257,9 +257,188 @@ void Check() { InitSucceeded = init(); } +bool QuietHoursEnabled = false; +DWORD QuietHoursValue = 0; + +[[nodiscard]] bool UseQuietHoursRegistryEntry() { + static const bool result = [] { + // Taken from QSysInfo. + OSVERSIONINFO result = { sizeof(OSVERSIONINFO), 0, 0, 0, 0,{ '\0' } }; + if (const auto library = GetModuleHandle(L"ntdll.dll")) { + using RtlGetVersionFunction = NTSTATUS(NTAPI*)(LPOSVERSIONINFO); + const auto RtlGetVersion = reinterpret_cast( + GetProcAddress(library, "RtlGetVersion")); + if (RtlGetVersion) { + RtlGetVersion(&result); + } + } + // At build 17134 (Redstone 4) the "Quiet hours" was replaced + // by "Focus assist" and it looks like it doesn't use registry. + return (result.dwMajorVersion == 10 + && result.dwMinorVersion == 0 + && result.dwBuildNumber < 17134); + }(); + return result; +} + +// Thanks https://stackoverflow.com/questions/35600128/get-windows-quiet-hours-from-win32-or-c-sharp-api +void QueryQuietHours() { + if (!UseQuietHoursRegistryEntry()) { + // There are quiet hours in Windows starting from Windows 8.1 + // But there were several reports about the notifications being shut + // down according to the registry while no quiet hours were enabled. + // So we try this method only starting with Windows 10. + return; + } + + LPCWSTR lpKeyName = L"Software\\Microsoft\\Windows\\CurrentVersion\\Notifications\\Settings"; + LPCWSTR lpValueName = L"NOC_GLOBAL_SETTING_TOASTS_ENABLED"; + HKEY key; + auto result = RegOpenKeyEx(HKEY_CURRENT_USER, lpKeyName, 0, KEY_READ, &key); + if (result != ERROR_SUCCESS) { + return; + } + + DWORD value = 0, type = 0, size = sizeof(value); + result = RegQueryValueEx(key, lpValueName, 0, &type, (LPBYTE)&value, &size); + RegCloseKey(key); + + auto quietHoursEnabled = (result == ERROR_SUCCESS) && (value == 0); + if (QuietHoursEnabled != quietHoursEnabled) { + QuietHoursEnabled = quietHoursEnabled; + QuietHoursValue = value; + LOG(("Quiet hours changed, entry value: %1").arg(value)); + } else if (QuietHoursValue != value) { + QuietHoursValue = value; + LOG(("Quiet hours value changed, was value: %1, entry value: %2").arg(QuietHoursValue).arg(value)); + } +} + +bool FocusAssistBlocks = false; + +// Thanks https://www.withinrafael.com/2019/09/19/determine-if-your-app-is-in-a-focus-assist-profiles-priority-list/ +void QueryFocusAssist() { + ComPtr quietHoursSettings; + auto hr = CoCreateInstance( + CLSID_QuietHoursSettings, + nullptr, + CLSCTX_LOCAL_SERVER, + IID_PPV_ARGS(&quietHoursSettings)); + if (!SUCCEEDED(hr) || !quietHoursSettings) { + return; + } + + auto profileId = LPWSTR{}; + const auto guardProfileId = gsl::finally([&] { + if (profileId) CoTaskMemFree(profileId); + }); + hr = quietHoursSettings->get_UserSelectedProfile(&profileId); + if (!SUCCEEDED(hr) || !profileId) { + return; + } + const auto profileName = QString::fromWCharArray(profileId); + if (profileName.endsWith(".alarmsonly", Qt::CaseInsensitive)) { + if (!FocusAssistBlocks) { + LOG(("Focus Assist: Alarms Only.")); + FocusAssistBlocks = true; + } + return; + } else if (!profileName.endsWith(".priorityonly", Qt::CaseInsensitive)) { + if (!profileName.endsWith(".unrestricted", Qt::CaseInsensitive)) { + LOG(("Focus Assist Warning: Unknown profile '%1'" + ).arg(profileName)); + } + if (FocusAssistBlocks) { + LOG(("Focus Assist: Unrestricted.")); + FocusAssistBlocks = false; + } + return; + } + const auto appUserModelId = std::wstring(AppUserModelId::getId()); + auto blocked = true; + const auto guard = gsl::finally([&] { + if (FocusAssistBlocks != blocked) { + LOG(("Focus Assist: %1, AppUserModelId: %2, Blocks: %3" + ).arg(profileName + ).arg(QString::fromStdWString(appUserModelId) + ).arg(Logs::b(blocked))); + FocusAssistBlocks = blocked; + } + }); + + ComPtr profile; + hr = quietHoursSettings->GetProfile(profileId, &profile); + if (!SUCCEEDED(hr) || !profile) { + return; + } + + UINT32 count = 0; + auto apps = (LPWSTR*)nullptr; + const auto guardApps = gsl::finally([&] { + if (apps) CoTaskMemFree(apps); + }); + hr = profile->GetAllowedApps(&count, &apps); + if (!SUCCEEDED(hr) || !apps) { + return; + } + for (UINT32 i = 0; i < count; i++) { + auto app = apps[i]; + const auto guardApp = gsl::finally([&] { + if (app) CoTaskMemFree(app); + }); + if (app == appUserModelId) { + blocked = false; + } + } +} + +QUERY_USER_NOTIFICATION_STATE UserNotificationState = QUNS_ACCEPTS_NOTIFICATIONS; + +void QueryUserNotificationState() { + if (Dlls::SHQueryUserNotificationState != nullptr) { + QUERY_USER_NOTIFICATION_STATE state; + if (SUCCEEDED(Dlls::SHQueryUserNotificationState(&state))) { + UserNotificationState = state; + } + } +} + +static constexpr auto kQuerySettingsEachMs = 1000; +crl::time LastSettingsQueryMs = 0; + +void QuerySystemNotificationSettings() { + auto ms = crl::now(); + if (LastSettingsQueryMs > 0 && ms <= LastSettingsQueryMs + kQuerySettingsEachMs) { + return; + } + LastSettingsQueryMs = ms; + QueryQuietHours(); + QueryFocusAssist(); + QueryUserNotificationState(); +} + } // namespace #endif // !__MINGW32__ +bool SkipAudioForCustom() { + QuerySystemNotificationSettings(); + + return (UserNotificationState == QUNS_NOT_PRESENT) + || (UserNotificationState == QUNS_PRESENTATION_MODE) + || Global::ScreenIsLocked(); +} + +bool SkipToastForCustom() { + QuerySystemNotificationSettings(); + + return (UserNotificationState == QUNS_PRESENTATION_MODE) + || (UserNotificationState == QUNS_RUNNING_D3D_FULL_SCREEN); +} + +bool SkipFlashBounceForCustom() { + return SkipToastForCustom(); +} + bool Supported() { #ifndef __MINGW32__ if (!Checked) { @@ -627,201 +806,23 @@ void Manager::onAfterNotificationActivated( not_null window) { _private->afterNotificationActivated(id, window); } + +bool Manager::doSkipAudio() const { + return SkipAudioForCustom() + || QuietHoursEnabled + || FocusAssistBlocks; +} + +bool Manager::doSkipToast() const { + return false; +} + +bool Manager::doSkipFlashBounce() const { + return SkipFlashBounceForCustom() + || QuietHoursEnabled + || FocusAssistBlocks; +} #endif // !__MINGW32__ -namespace { - -bool QuietHoursEnabled = false; -DWORD QuietHoursValue = 0; - -[[nodiscard]] bool UseQuietHoursRegistryEntry() { - static const bool result = [] { - // Taken from QSysInfo. - OSVERSIONINFO result = { sizeof(OSVERSIONINFO), 0, 0, 0, 0,{ '\0' } }; - if (const auto library = GetModuleHandle(L"ntdll.dll")) { - using RtlGetVersionFunction = NTSTATUS(NTAPI*)(LPOSVERSIONINFO); - const auto RtlGetVersion = reinterpret_cast( - GetProcAddress(library, "RtlGetVersion")); - if (RtlGetVersion) { - RtlGetVersion(&result); - } - } - // At build 17134 (Redstone 4) the "Quiet hours" was replaced - // by "Focus assist" and it looks like it doesn't use registry. - return (result.dwMajorVersion == 10 - && result.dwMinorVersion == 0 - && result.dwBuildNumber < 17134); - }(); - return result; -} - -// Thanks https://stackoverflow.com/questions/35600128/get-windows-quiet-hours-from-win32-or-c-sharp-api -void QueryQuietHours() { - if (!UseQuietHoursRegistryEntry()) { - // There are quiet hours in Windows starting from Windows 8.1 - // But there were several reports about the notifications being shut - // down according to the registry while no quiet hours were enabled. - // So we try this method only starting with Windows 10. - return; - } - - LPCWSTR lpKeyName = L"Software\\Microsoft\\Windows\\CurrentVersion\\Notifications\\Settings"; - LPCWSTR lpValueName = L"NOC_GLOBAL_SETTING_TOASTS_ENABLED"; - HKEY key; - auto result = RegOpenKeyEx(HKEY_CURRENT_USER, lpKeyName, 0, KEY_READ, &key); - if (result != ERROR_SUCCESS) { - return; - } - - DWORD value = 0, type = 0, size = sizeof(value); - result = RegQueryValueEx(key, lpValueName, 0, &type, (LPBYTE)&value, &size); - RegCloseKey(key); - - auto quietHoursEnabled = (result == ERROR_SUCCESS) && (value == 0); - if (QuietHoursEnabled != quietHoursEnabled) { - QuietHoursEnabled = quietHoursEnabled; - QuietHoursValue = value; - LOG(("Quiet hours changed, entry value: %1").arg(value)); - } else if (QuietHoursValue != value) { - QuietHoursValue = value; - LOG(("Quiet hours value changed, was value: %1, entry value: %2").arg(QuietHoursValue).arg(value)); - } -} - -bool FocusAssistBlocks = false; - -// Thanks https://www.withinrafael.com/2019/09/19/determine-if-your-app-is-in-a-focus-assist-profiles-priority-list/ -void QueryFocusAssist() { - ComPtr quietHoursSettings; - auto hr = CoCreateInstance( - CLSID_QuietHoursSettings, - nullptr, - CLSCTX_LOCAL_SERVER, - IID_PPV_ARGS(&quietHoursSettings)); - if (!SUCCEEDED(hr) || !quietHoursSettings) { - return; - } - - auto profileId = LPWSTR{}; - const auto guardProfileId = gsl::finally([&] { - if (profileId) CoTaskMemFree(profileId); - }); - hr = quietHoursSettings->get_UserSelectedProfile(&profileId); - if (!SUCCEEDED(hr) || !profileId) { - return; - } - const auto profileName = QString::fromWCharArray(profileId); - if (profileName.endsWith(".alarmsonly", Qt::CaseInsensitive)) { - if (!FocusAssistBlocks) { - LOG(("Focus Assist: Alarms Only.")); - FocusAssistBlocks = true; - } - return; - } else if (!profileName.endsWith(".priorityonly", Qt::CaseInsensitive)) { - if (!profileName.endsWith(".unrestricted", Qt::CaseInsensitive)) { - LOG(("Focus Assist Warning: Unknown profile '%1'" - ).arg(profileName)); - } - if (FocusAssistBlocks) { - LOG(("Focus Assist: Unrestricted.")); - FocusAssistBlocks = false; - } - return; - } - const auto appUserModelId = std::wstring(AppUserModelId::getId()); - auto blocked = true; - const auto guard = gsl::finally([&] { - if (FocusAssistBlocks != blocked) { - LOG(("Focus Assist: %1, AppUserModelId: %2, Blocks: %3" - ).arg(profileName - ).arg(QString::fromStdWString(appUserModelId) - ).arg(Logs::b(blocked))); - FocusAssistBlocks = blocked; - } - }); - - ComPtr profile; - hr = quietHoursSettings->GetProfile(profileId, &profile); - if (!SUCCEEDED(hr) || !profile) { - return; - } - - UINT32 count = 0; - auto apps = (LPWSTR*)nullptr; - const auto guardApps = gsl::finally([&] { - if (apps) CoTaskMemFree(apps); - }); - hr = profile->GetAllowedApps(&count, &apps); - if (!SUCCEEDED(hr) || !apps) { - return; - } - for (UINT32 i = 0; i < count; i++) { - auto app = apps[i]; - const auto guardApp = gsl::finally([&] { - if (app) CoTaskMemFree(app); - }); - if (app == appUserModelId) { - blocked = false; - } - } -} - -QUERY_USER_NOTIFICATION_STATE UserNotificationState = QUNS_ACCEPTS_NOTIFICATIONS; - -void QueryUserNotificationState() { - if (Dlls::SHQueryUserNotificationState != nullptr) { - QUERY_USER_NOTIFICATION_STATE state; - if (SUCCEEDED(Dlls::SHQueryUserNotificationState(&state))) { - UserNotificationState = state; - } - } -} - -static constexpr auto kQuerySettingsEachMs = 1000; -crl::time LastSettingsQueryMs = 0; - -void QuerySystemNotificationSettings() { - auto ms = crl::now(); - if (LastSettingsQueryMs > 0 && ms <= LastSettingsQueryMs + kQuerySettingsEachMs) { - return; - } - LastSettingsQueryMs = ms; - QueryQuietHours(); - QueryFocusAssist(); - QueryUserNotificationState(); -} - -} // namespace - -bool SkipAudio() { - QuerySystemNotificationSettings(); - - if (UserNotificationState == QUNS_NOT_PRESENT - || UserNotificationState == QUNS_PRESENTATION_MODE - || QuietHoursEnabled - || FocusAssistBlocks - || Global::ScreenIsLocked()) { - return true; - } - return false; -} - -bool SkipToast() { - QuerySystemNotificationSettings(); - - if (UserNotificationState == QUNS_PRESENTATION_MODE - || UserNotificationState == QUNS_RUNNING_D3D_FULL_SCREEN - //|| UserNotificationState == QUNS_BUSY - || QuietHoursEnabled - || FocusAssistBlocks) { - return true; - } - return false; -} - -bool SkipFlashBounce() { - return SkipToast(); -} - } // namespace Notifications } // namespace Platform diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.h b/Telegram/SourceFiles/platform/win/notifications_manager_win.h index b86a30221f..be37f9ff42 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.h +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.h @@ -39,6 +39,9 @@ protected: void onAfterNotificationActivated( NotificationId id, not_null window) override; + bool doSkipAudio() const override; + bool doSkipToast() const override; + bool doSkipFlashBounce() const override; private: class Private; diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 040533a940..f2d6896d1c 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -138,6 +138,8 @@ System::SkipState System::skipNotification( } void System::schedule(not_null item) { + Expects(_manager != nullptr); + const auto history = item->history(); const auto skip = skipNotification(item); if (skip.value == SkipState::Skip) { @@ -167,7 +169,7 @@ void System::schedule(not_null item) { _whenAlerts[history].emplace(when, notifyBy); } if (Core::App().settings().desktopNotify() - && !Platform::Notifications::SkipToast()) { + && !_manager->skipToast()) { auto &whenMap = _whenMaps[history]; if (whenMap.find(item->id) == whenMap.end()) { whenMap.emplace(item->id, when); @@ -391,7 +393,7 @@ void System::showNext() { } const auto &settings = Core::App().settings(); if (alert) { - if (settings.flashBounceNotify() && !Platform::Notifications::SkipFlashBounce()) { + if (settings.flashBounceNotify() && !_manager->skipFlashBounce()) { if (const auto window = Core::App().activeWindow()) { if (const auto handle = window->widget()->windowHandle()) { handle->alert(kSystemAlertDuration); @@ -399,7 +401,7 @@ void System::showNext() { } } } - if (settings.soundNotify() && !Platform::Notifications::SkipAudio()) { + if (settings.soundNotify() && !_manager->skipAudio()) { ensureSoundCreated(); _soundTrack->playOnce(); Media::Player::mixer()->suppressAll(_soundTrack->getLengthMs()); @@ -407,7 +409,7 @@ void System::showNext() { } } - if (_waiters.empty() || !settings.desktopNotify() || Platform::Notifications::SkipToast()) { + if (_waiters.empty() || !settings.desktopNotify() || _manager->skipToast()) { if (nextAlert) { _waitTimer.callOnce(nextAlert - ms); } diff --git a/Telegram/SourceFiles/window/notifications_manager.h b/Telegram/SourceFiles/window/notifications_manager.h index 83c8ad3479..8c0d338cff 100644 --- a/Telegram/SourceFiles/window/notifications_manager.h +++ b/Telegram/SourceFiles/window/notifications_manager.h @@ -202,6 +202,16 @@ public: [[nodiscard]] virtual ManagerType type() const = 0; + [[nodiscard]] bool skipAudio() const { + return doSkipAudio(); + } + [[nodiscard]] bool skipToast() const { + return doSkipToast(); + } + [[nodiscard]] bool skipFlashBounce() const { + return doSkipFlashBounce(); + } + virtual ~Manager() = default; protected: @@ -218,6 +228,9 @@ protected: virtual void doClearFromItem(not_null item) = 0; virtual void doClearFromHistory(not_null history) = 0; virtual void doClearFromSession(not_null session) = 0; + virtual bool doSkipAudio() const = 0; + virtual bool doSkipToast() const = 0; + virtual bool doSkipFlashBounce() const = 0; [[nodiscard]] virtual bool forceHideDetails() const { return false; } @@ -298,6 +311,15 @@ protected: } void doClearFromSession(not_null session) override { } + bool doSkipAudio() const { + return false; + } + bool doSkipToast() const { + return false; + } + bool doSkipFlashBounce() const { + return false; + } }; diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index 5c9f3cb207..457e7a5619 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -404,6 +404,18 @@ void Manager::doClearFromItem(not_null item) { } } +bool Manager::doSkipAudio() const { + return Platform::Notifications::SkipAudioForCustom(); +} + +bool Manager::doSkipToast() const { + return Platform::Notifications::SkipToastForCustom(); +} + +bool Manager::doSkipFlashBounce() const { + return Platform::Notifications::SkipFlashBounceForCustom(); +} + void Manager::doUpdateAll() { for_const (auto ¬ification, _notifications) { notification->updateNotifyDisplay(); diff --git a/Telegram/SourceFiles/window/notifications_manager_default.h b/Telegram/SourceFiles/window/notifications_manager_default.h index 68d7c96142..19a6a98c0f 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.h +++ b/Telegram/SourceFiles/window/notifications_manager_default.h @@ -76,6 +76,9 @@ private: void doClearFromHistory(not_null history) override; void doClearFromSession(not_null session) override; void doClearFromItem(not_null item) override; + bool doSkipAudio() const; + bool doSkipToast() const; + bool doSkipFlashBounce() const; void showNextFromQueue(); void unlinkFromShown(Notification *remove);