996 lines
		
	
	
	
		
			26 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			996 lines
		
	
	
	
		
			26 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
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/win/notifications_manager_win.h"
 | 
						|
 | 
						|
#include "window/notifications_utilities.h"
 | 
						|
#include "window/window_session_controller.h"
 | 
						|
#include "base/platform/win/base_windows_co_task_mem.h"
 | 
						|
#include "base/platform/win/base_windows_rpcndr_h.h"
 | 
						|
#include "base/platform/win/base_windows_winrt.h"
 | 
						|
#include "base/platform/base_platform_info.h"
 | 
						|
#include "base/platform/win/wrl/wrl_module_h.h"
 | 
						|
#include "base/qthelp_url.h"
 | 
						|
#include "platform/win/windows_app_user_model_id.h"
 | 
						|
#include "platform/win/windows_toast_activator.h"
 | 
						|
#include "platform/win/windows_dlls.h"
 | 
						|
#include "platform/win/specific_win.h"
 | 
						|
#include "data/data_forum_topic.h"
 | 
						|
#include "history/history.h"
 | 
						|
#include "history/history_item.h"
 | 
						|
#include "core/application.h"
 | 
						|
#include "core/core_settings.h"
 | 
						|
#include "lang/lang_keys.h"
 | 
						|
#include "main/main_session.h"
 | 
						|
#include "mainwindow.h"
 | 
						|
#include "windows_quiethours_h.h"
 | 
						|
#include "styles/style_chat.h"
 | 
						|
#include "styles/style_chat_helpers.h"
 | 
						|
 | 
						|
#include <QtCore/QOperatingSystemVersion>
 | 
						|
 | 
						|
#include <Shobjidl.h>
 | 
						|
#include <shellapi.h>
 | 
						|
#include <strsafe.h>
 | 
						|
 | 
						|
#ifndef __MINGW32__
 | 
						|
#include <winrt/Windows.Foundation.h>
 | 
						|
#include <winrt/Windows.Data.Xml.Dom.h>
 | 
						|
#include <winrt/Windows.UI.Notifications.h>
 | 
						|
 | 
						|
HICON qt_pixmapToWinHICON(const QPixmap &);
 | 
						|
 | 
						|
using namespace winrt::Windows::UI::Notifications;
 | 
						|
using namespace winrt::Windows::Data::Xml::Dom;
 | 
						|
using namespace winrt::Windows::Foundation;
 | 
						|
using winrt::com_ptr;
 | 
						|
#endif // !__MINGW32__
 | 
						|
 | 
						|
namespace Platform {
 | 
						|
namespace Notifications {
 | 
						|
 | 
						|
#ifndef __MINGW32__
 | 
						|
namespace {
 | 
						|
 | 
						|
constexpr auto kQuerySettingsEachMs = 1000;
 | 
						|
 | 
						|
crl::time LastSettingsQueryMs/* = 0*/;
 | 
						|
 | 
						|
[[nodiscard]] bool ShouldQuerySettings() {
 | 
						|
	const auto now = crl::now();
 | 
						|
	if (LastSettingsQueryMs > 0 && now <= LastSettingsQueryMs + kQuerySettingsEachMs) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	LastSettingsQueryMs = now;
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
[[nodiscard]] std::wstring NotificationTemplate(
 | 
						|
		QString id,
 | 
						|
		Window::Notifications::Manager::DisplayOptions options) {
 | 
						|
	const auto wid = id.replace('&', "&").toStdWString();
 | 
						|
	const auto fastReply = LR"(
 | 
						|
		<input id="fastReply" type="text" placeHolderContent=""/>
 | 
						|
		<action
 | 
						|
			content="Send"
 | 
						|
			arguments="action=reply&)" + wid + LR"("
 | 
						|
			activationType="background"
 | 
						|
			imageUri=""
 | 
						|
			hint-inputId="fastReply"/>
 | 
						|
)";
 | 
						|
	const auto markAsRead = LR"(
 | 
						|
        <action
 | 
						|
            content=""
 | 
						|
            arguments="action=mark&)" + wid + LR"("
 | 
						|
            activationType="background"/>
 | 
						|
)";
 | 
						|
	const auto actions = (options.hideReplyButton ? L"" : fastReply)
 | 
						|
		+ (options.hideMarkAsRead ? L"" : markAsRead);
 | 
						|
	return LR"(
 | 
						|
<toast launch="action=open&)" + wid + LR"(">
 | 
						|
	<visual>
 | 
						|
		<binding template="ToastGeneric">
 | 
						|
			<image placement="appLogoOverride" hint-crop="circle" src=""/>
 | 
						|
			<text hint-maxLines="1"></text>
 | 
						|
			<text></text>
 | 
						|
			<text></text>
 | 
						|
		</binding>
 | 
						|
	</visual>
 | 
						|
)" + (actions.empty()
 | 
						|
	? L""
 | 
						|
	: (L"<actions>" + actions + L"</actions>")) + LR"(
 | 
						|
	<audio silent="true"/>
 | 
						|
</toast>
 | 
						|
)";
 | 
						|
}
 | 
						|
 | 
						|
bool init() {
 | 
						|
	if (!IsWindows8OrGreater() || !base::WinRT::Supported()) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	{
 | 
						|
		using namespace Microsoft::WRL;
 | 
						|
		const auto hr = Module<OutOfProc>::GetModule().RegisterObjects();
 | 
						|
		if (!SUCCEEDED(hr)) {
 | 
						|
			LOG(("App Error: Object registration failed."));
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (!AppUserModelId::ValidateShortcut()) {
 | 
						|
		LOG(("App Error: Shortcut validation failed."));
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	PWSTR appUserModelId = {};
 | 
						|
	if (!SUCCEEDED(GetCurrentProcessExplicitAppUserModelID(&appUserModelId))) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	const auto appUserModelIdGuard = gsl::finally([&] {
 | 
						|
		CoTaskMemFree(appUserModelId);
 | 
						|
	});
 | 
						|
 | 
						|
	if (AppUserModelId::Id() != appUserModelId) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
// Throws.
 | 
						|
void SetNodeValueString(
 | 
						|
		const XmlDocument &xml,
 | 
						|
		const IXmlNode &node,
 | 
						|
		const std::wstring &text) {
 | 
						|
	node.AppendChild(xml.CreateTextNode(text).as<IXmlNode>());
 | 
						|
}
 | 
						|
 | 
						|
// Throws.
 | 
						|
void SetAudioSilent(const XmlDocument &toastXml) {
 | 
						|
	const auto nodeList = toastXml.GetElementsByTagName(L"audio");
 | 
						|
	if (const auto audioNode = nodeList.Item(0)) {
 | 
						|
		audioNode.as<IXmlElement>().SetAttribute(L"silent", L"true");
 | 
						|
	} else {
 | 
						|
		auto audioElement = toastXml.CreateElement(L"audio");
 | 
						|
		audioElement.SetAttribute(L"silent", L"true");
 | 
						|
		auto nodeList = toastXml.GetElementsByTagName(L"toast");
 | 
						|
		nodeList.Item(0).AppendChild(audioElement.as<IXmlNode>());
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Throws.
 | 
						|
void SetImageSrc(const XmlDocument &toastXml, const std::wstring &path) {
 | 
						|
	const auto nodeList = toastXml.GetElementsByTagName(L"image");
 | 
						|
	const auto attributes = nodeList.Item(0).Attributes();
 | 
						|
	return SetNodeValueString(
 | 
						|
		toastXml,
 | 
						|
		attributes.GetNamedItem(L"src"),
 | 
						|
		L"file:///" + path);
 | 
						|
}
 | 
						|
 | 
						|
// Throws.
 | 
						|
void SetReplyIconSrc(const XmlDocument &toastXml, const std::wstring &path) {
 | 
						|
	const auto nodeList = toastXml.GetElementsByTagName(L"action");
 | 
						|
	const auto length = int(nodeList.Length());
 | 
						|
	for (auto i = 0; i != length; ++i) {
 | 
						|
		const auto attributes = nodeList.Item(i).Attributes();
 | 
						|
		if (const auto uri = attributes.GetNamedItem(L"imageUri")) {
 | 
						|
			return SetNodeValueString(toastXml, uri, L"file:///" + path);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Throws.
 | 
						|
void SetReplyPlaceholder(
 | 
						|
		const XmlDocument &toastXml,
 | 
						|
		const std::wstring &placeholder) {
 | 
						|
	const auto nodeList = toastXml.GetElementsByTagName(L"input");
 | 
						|
	const auto attributes = nodeList.Item(0).Attributes();
 | 
						|
	return SetNodeValueString(
 | 
						|
		toastXml,
 | 
						|
		attributes.GetNamedItem(L"placeHolderContent"),
 | 
						|
		placeholder);
 | 
						|
}
 | 
						|
 | 
						|
// Throws.
 | 
						|
void SetAction(const XmlDocument &toastXml, const QString &id) {
 | 
						|
	auto nodeList = toastXml.GetElementsByTagName(L"toast");
 | 
						|
	if (const auto toast = nodeList.Item(0).try_as<XmlElement>()) {
 | 
						|
		toast.SetAttribute(L"launch", L"action=open&" + id.toStdWString());
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Throws.
 | 
						|
void SetMarkAsReadText(
 | 
						|
		const XmlDocument &toastXml,
 | 
						|
		const std::wstring &text) {
 | 
						|
	const auto nodeList = toastXml.GetElementsByTagName(L"action");
 | 
						|
	const auto length = int(nodeList.Length());
 | 
						|
	for (auto i = 0; i != length; ++i) {
 | 
						|
		const auto attributes = nodeList.Item(i).Attributes();
 | 
						|
		if (!attributes.GetNamedItem(L"imageUri")) {
 | 
						|
			return SetNodeValueString(
 | 
						|
				toastXml,
 | 
						|
				attributes.GetNamedItem(L"content"),
 | 
						|
				text);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
auto Checked = false;
 | 
						|
auto InitSucceeded = false;
 | 
						|
 | 
						|
void Check() {
 | 
						|
	InitSucceeded = init();
 | 
						|
}
 | 
						|
 | 
						|
bool QuietHoursEnabled = false;
 | 
						|
DWORD QuietHoursValue = 0;
 | 
						|
 | 
						|
[[nodiscard]] bool UseQuietHoursRegistryEntry() {
 | 
						|
	static const bool result = [] {
 | 
						|
		const auto version = QOperatingSystemVersion::current();
 | 
						|
 | 
						|
		// At build 17134 (Redstone 4) the "Quiet hours" was replaced
 | 
						|
		// by "Focus assist" and it looks like it doesn't use registry.
 | 
						|
		return (version.majorVersion() == 10)
 | 
						|
			&& (version.minorVersion() == 0)
 | 
						|
			&& (version.microVersion() < 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() {
 | 
						|
	const auto quietHoursSettings = base::WinRT::TryCreateInstance<
 | 
						|
		IQuietHoursSettings
 | 
						|
	>(CLSID_QuietHoursSettings, CLSCTX_LOCAL_SERVER);
 | 
						|
	if (!quietHoursSettings) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	auto profileId = base::CoTaskMemString();
 | 
						|
	auto hr = quietHoursSettings->get_UserSelectedProfile(profileId.put());
 | 
						|
	if (FAILED(hr) || !profileId) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto profileName = QString::fromWCharArray(profileId.data());
 | 
						|
	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 = AppUserModelId::Id();
 | 
						|
	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;
 | 
						|
		}
 | 
						|
	});
 | 
						|
 | 
						|
	com_ptr<IQuietHoursProfile> profile;
 | 
						|
	hr = quietHoursSettings->GetProfile(profileId.data(), profile.put());
 | 
						|
	if (FAILED(hr) || !profile) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	auto apps = base::CoTaskMemStringArray();
 | 
						|
	hr = profile->GetAllowedApps(apps.put_size(), apps.put());
 | 
						|
	if (FAILED(hr) || !apps) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	for (const auto &app : apps) {
 | 
						|
		if (app && app.data() == appUserModelId) {
 | 
						|
			blocked = false;
 | 
						|
			break;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void QuerySystemNotificationSettings() {
 | 
						|
	if (!ShouldQuerySettings()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	QueryQuietHours();
 | 
						|
	QueryFocusAssist();
 | 
						|
	QueryUserNotificationState();
 | 
						|
}
 | 
						|
 | 
						|
bool SkipSoundForCustom() {
 | 
						|
	QuerySystemNotificationSettings();
 | 
						|
 | 
						|
	return (UserNotificationState == QUNS_NOT_PRESENT)
 | 
						|
		|| (UserNotificationState == QUNS_PRESENTATION_MODE)
 | 
						|
		|| Core::App().screenIsLocked();
 | 
						|
}
 | 
						|
 | 
						|
bool SkipFlashBounceForCustom() {
 | 
						|
	return SkipToastForCustom();
 | 
						|
}
 | 
						|
 | 
						|
} // namespace
 | 
						|
#endif // !__MINGW32__
 | 
						|
 | 
						|
void MaybePlaySoundForCustom(Fn<void()> playSound) {
 | 
						|
	if (!SkipSoundForCustom()) {
 | 
						|
		playSound();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
bool SkipToastForCustom() {
 | 
						|
	QuerySystemNotificationSettings();
 | 
						|
 | 
						|
	return (UserNotificationState == QUNS_PRESENTATION_MODE)
 | 
						|
		|| (UserNotificationState == QUNS_RUNNING_D3D_FULL_SCREEN);
 | 
						|
}
 | 
						|
 | 
						|
void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
 | 
						|
	if (!SkipFlashBounceForCustom()) {
 | 
						|
		flashBounce();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
bool WaitForInputForCustom() {
 | 
						|
	QuerySystemNotificationSettings();
 | 
						|
 | 
						|
	return UserNotificationState != QUNS_BUSY;
 | 
						|
}
 | 
						|
 | 
						|
bool Supported() {
 | 
						|
#ifndef __MINGW32__
 | 
						|
	if (!Checked) {
 | 
						|
		Checked = true;
 | 
						|
		Check();
 | 
						|
	}
 | 
						|
	return InitSucceeded;
 | 
						|
#endif // !__MINGW32__
 | 
						|
 | 
						|
	return false;
 | 
						|
}
 | 
						|
 | 
						|
bool Enforced() {
 | 
						|
	return false;
 | 
						|
}
 | 
						|
 | 
						|
bool ByDefault() {
 | 
						|
	return false;
 | 
						|
}
 | 
						|
 | 
						|
void Create(Window::Notifications::System *system) {
 | 
						|
#ifndef __MINGW32__
 | 
						|
	if (Core::App().settings().nativeNotifications() && Supported()) {
 | 
						|
		auto result = std::make_unique<Manager>(system);
 | 
						|
		if (result->init()) {
 | 
						|
			system->setManager(std::move(result));
 | 
						|
			return;
 | 
						|
		}
 | 
						|
	}
 | 
						|
#endif // !__MINGW32__
 | 
						|
	system->setManager(nullptr);
 | 
						|
}
 | 
						|
 | 
						|
#ifndef __MINGW32__
 | 
						|
class Manager::Private {
 | 
						|
public:
 | 
						|
	explicit Private(Manager *instance);
 | 
						|
	bool init();
 | 
						|
 | 
						|
	bool 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 beforeNotificationActivated(NotificationId id);
 | 
						|
	void afterNotificationActivated(
 | 
						|
		NotificationId id,
 | 
						|
		not_null<Window::SessionController*> window);
 | 
						|
	void clearNotification(NotificationId id);
 | 
						|
 | 
						|
	void handleActivation(const ToastActivation &activation);
 | 
						|
 | 
						|
	~Private();
 | 
						|
 | 
						|
private:
 | 
						|
	bool showNotificationInTryCatch(
 | 
						|
		not_null<PeerData*> peer,
 | 
						|
		MsgId topicRootId,
 | 
						|
		Ui::PeerUserpicView &userpicView,
 | 
						|
		MsgId msgId,
 | 
						|
		const QString &title,
 | 
						|
		const QString &subtitle,
 | 
						|
		const QString &msg,
 | 
						|
		DisplayOptions options);
 | 
						|
	void tryHide(const ToastNotification ¬ification);
 | 
						|
	[[nodiscard]] std::wstring ensureSendButtonIcon();
 | 
						|
 | 
						|
	Window::Notifications::CachedUserpics _cachedUserpics;
 | 
						|
	std::wstring _sendButtonIconPath;
 | 
						|
 | 
						|
	std::shared_ptr<Manager*> _guarded;
 | 
						|
	ToastNotifier _notifier = nullptr;
 | 
						|
 | 
						|
	base::flat_map<
 | 
						|
		ContextId,
 | 
						|
		base::flat_map<MsgId, ToastNotification>> _notifications;
 | 
						|
	rpl::lifetime _lifetime;
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
Manager::Private::Private(Manager *instance)
 | 
						|
: _guarded(std::make_shared<Manager*>(instance)) {
 | 
						|
	ToastActivations(
 | 
						|
	) | rpl::start_with_next([=](const ToastActivation &activation) {
 | 
						|
		handleActivation(activation);
 | 
						|
	}, _lifetime);
 | 
						|
}
 | 
						|
 | 
						|
bool Manager::Private::init() {
 | 
						|
	return base::WinRT::Try([&] {
 | 
						|
		_notifier = ToastNotificationManager::CreateToastNotifier(
 | 
						|
			AppUserModelId::Id());
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
Manager::Private::~Private() {
 | 
						|
	clearAll();
 | 
						|
 | 
						|
	_notifications.clear();
 | 
						|
	_notifier = nullptr;
 | 
						|
}
 | 
						|
 | 
						|
void Manager::Private::clearAll() {
 | 
						|
	if (!_notifier) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	for (const auto &[key, notifications] : base::take(_notifications)) {
 | 
						|
		for (const auto &[msgId, notification] : notifications) {
 | 
						|
			tryHide(notification);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
 | 
						|
	if (!_notifier) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	auto i = _notifications.find(ContextId{
 | 
						|
		.sessionId = item->history()->session().uniqueId(),
 | 
						|
		.peerId = item->history()->peer->id,
 | 
						|
		.topicRootId = item->topicRootId(),
 | 
						|
	});
 | 
						|
	if (i == _notifications.cend()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto j = i->second.find(item->id);
 | 
						|
	if (j == end(i->second)) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto taken = std::exchange(j->second, nullptr);
 | 
						|
	i->second.erase(j);
 | 
						|
	if (i->second.empty()) {
 | 
						|
		_notifications.erase(i);
 | 
						|
	}
 | 
						|
	tryHide(taken);
 | 
						|
}
 | 
						|
 | 
						|
void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
 | 
						|
	if (!_notifier) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	const auto i = _notifications.find(ContextId{
 | 
						|
		.sessionId = topic->session().uniqueId(),
 | 
						|
		.peerId = topic->history()->peer->id,
 | 
						|
		.topicRootId = topic->rootId(),
 | 
						|
	});
 | 
						|
	if (i != _notifications.cend()) {
 | 
						|
		const auto temp = base::take(i->second);
 | 
						|
		_notifications.erase(i);
 | 
						|
 | 
						|
		for (const auto &[msgId, notification] : temp) {
 | 
						|
			tryHide(notification);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Manager::Private::clearFromHistory(not_null<History*> history) {
 | 
						|
	if (!_notifier) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	const auto sessionId = history->session().uniqueId();
 | 
						|
	const auto peerId = history->peer->id;
 | 
						|
	auto i = _notifications.lower_bound(ContextId{
 | 
						|
		.sessionId = sessionId,
 | 
						|
		.peerId = peerId,
 | 
						|
	});
 | 
						|
	while (i != _notifications.cend()
 | 
						|
		&& i->first.sessionId == sessionId
 | 
						|
		&& i->first.peerId == peerId) {
 | 
						|
		const auto temp = base::take(i->second);
 | 
						|
		i = _notifications.erase(i);
 | 
						|
 | 
						|
		for (const auto &[msgId, notification] : temp) {
 | 
						|
			tryHide(notification);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
 | 
						|
	if (!_notifier) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	const auto sessionId = session->uniqueId();
 | 
						|
	auto i = _notifications.lower_bound(ContextId{
 | 
						|
		.sessionId = sessionId,
 | 
						|
	});
 | 
						|
	while (i != _notifications.cend() && i->first.sessionId == sessionId) {
 | 
						|
		const auto temp = base::take(i->second);
 | 
						|
		i = _notifications.erase(i);
 | 
						|
 | 
						|
		for (const auto &[msgId, notification] : temp) {
 | 
						|
			tryHide(notification);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Manager::Private::beforeNotificationActivated(NotificationId id) {
 | 
						|
	clearNotification(id);
 | 
						|
}
 | 
						|
 | 
						|
void Manager::Private::afterNotificationActivated(
 | 
						|
		NotificationId id,
 | 
						|
		not_null<Window::SessionController*> window) {
 | 
						|
	SetForegroundWindow(window->widget()->psHwnd());
 | 
						|
}
 | 
						|
 | 
						|
void Manager::Private::clearNotification(NotificationId id) {
 | 
						|
	auto i = _notifications.find(id.contextId);
 | 
						|
	if (i != _notifications.cend()) {
 | 
						|
		i->second.remove(id.msgId);
 | 
						|
		if (i->second.empty()) {
 | 
						|
			_notifications.erase(i);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Manager::Private::handleActivation(const ToastActivation &activation) {
 | 
						|
	const auto parsed = qthelp::url_parse_params(activation.args);
 | 
						|
	const auto pid = parsed.value("pid").toULong();
 | 
						|
	const auto my = GetCurrentProcessId();
 | 
						|
	if (pid != my) {
 | 
						|
		DEBUG_LOG(("Toast Info: "
 | 
						|
			"Got activation \"%1\", my %2, activating %3."
 | 
						|
			).arg(activation.args
 | 
						|
			).arg(my
 | 
						|
			).arg(pid));
 | 
						|
		const auto processId = pid;
 | 
						|
		const auto windowId = 0; // Activate some window.
 | 
						|
		Platform::ActivateOtherProcess(processId, windowId);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto action = parsed.value("action");
 | 
						|
	const auto id = NotificationId{
 | 
						|
		.contextId = ContextId{
 | 
						|
			.sessionId = parsed.value("session").toULongLong(),
 | 
						|
			.peerId = PeerId(parsed.value("peer").toULongLong()),
 | 
						|
			.topicRootId = MsgId(parsed.value("topic").toLongLong())
 | 
						|
		},
 | 
						|
		.msgId = MsgId(parsed.value("msg").toLongLong()),
 | 
						|
	};
 | 
						|
	if (!id.contextId.sessionId || !id.contextId.peerId || !id.msgId) {
 | 
						|
		DEBUG_LOG(("Toast Info: Got activation \"%1\", my %1, skipping."
 | 
						|
			).arg(activation.args
 | 
						|
			).arg(pid));
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	DEBUG_LOG(("Toast Info: Got activation \"%1\", my %1, handling."
 | 
						|
		).arg(activation.args
 | 
						|
		).arg(pid));
 | 
						|
	auto text = TextWithTags();
 | 
						|
	for (const auto &entry : activation.input) {
 | 
						|
		if (entry.key == "fastReply") {
 | 
						|
			text.text = entry.value;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	const auto i = _notifications.find(id.contextId);
 | 
						|
	if (i == _notifications.cend() || !i->second.contains(id.msgId)) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	const auto manager = *_guarded;
 | 
						|
	if (action == "reply") {
 | 
						|
		manager->notificationReplied(id, text);
 | 
						|
	} else if (action == "mark") {
 | 
						|
		manager->notificationReplied(id, TextWithTags());
 | 
						|
	} else {
 | 
						|
		manager->notificationActivated(id, text);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
bool 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) {
 | 
						|
	if (!_notifier) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	return base::WinRT::Try([&] {
 | 
						|
		return showNotificationInTryCatch(
 | 
						|
			peer,
 | 
						|
			topicRootId,
 | 
						|
			userpicView,
 | 
						|
			msgId,
 | 
						|
			title,
 | 
						|
			subtitle,
 | 
						|
			msg,
 | 
						|
			options);
 | 
						|
	}).value_or(false);
 | 
						|
}
 | 
						|
 | 
						|
std::wstring Manager::Private::ensureSendButtonIcon() {
 | 
						|
	if (_sendButtonIconPath.empty()) {
 | 
						|
		const auto path = cWorkingDir() + u"tdata/temp/fast_reply.png"_q;
 | 
						|
		st::historySendIcon.instance(Qt::white, 300).save(path, "PNG");
 | 
						|
		_sendButtonIconPath = path.toStdWString();
 | 
						|
	}
 | 
						|
	return _sendButtonIconPath;
 | 
						|
}
 | 
						|
 | 
						|
bool Manager::Private::showNotificationInTryCatch(
 | 
						|
		not_null<PeerData*> peer,
 | 
						|
		MsgId topicRootId,
 | 
						|
		Ui::PeerUserpicView &userpicView,
 | 
						|
		MsgId msgId,
 | 
						|
		const QString &title,
 | 
						|
		const QString &subtitle,
 | 
						|
		const QString &msg,
 | 
						|
		DisplayOptions options) {
 | 
						|
	const auto withSubtitle = !subtitle.isEmpty();
 | 
						|
	auto toastXml = XmlDocument();
 | 
						|
 | 
						|
	const auto key = ContextId{
 | 
						|
		.sessionId = peer->session().uniqueId(),
 | 
						|
		.peerId = peer->id,
 | 
						|
		.topicRootId = topicRootId,
 | 
						|
	};
 | 
						|
	const auto notificationId = NotificationId{
 | 
						|
		.contextId = key,
 | 
						|
		.msgId = msgId
 | 
						|
	};
 | 
						|
	const auto idString = u"pid=%1&session=%2&peer=%3&topic=%4&msg=%5"_q
 | 
						|
		.arg(GetCurrentProcessId())
 | 
						|
		.arg(key.sessionId)
 | 
						|
		.arg(key.peerId.value)
 | 
						|
		.arg(topicRootId.bare)
 | 
						|
		.arg(msgId.bare);
 | 
						|
 | 
						|
	const auto modern = Platform::IsWindows10OrGreater();
 | 
						|
	if (modern) {
 | 
						|
		toastXml.LoadXml(NotificationTemplate(idString, options));
 | 
						|
	} else {
 | 
						|
		toastXml = ToastNotificationManager::GetTemplateContent(
 | 
						|
			(withSubtitle
 | 
						|
				? ToastTemplateType::ToastImageAndText04
 | 
						|
				: ToastTemplateType::ToastImageAndText02));
 | 
						|
		SetAudioSilent(toastXml);
 | 
						|
		SetAction(toastXml, idString);
 | 
						|
	}
 | 
						|
 | 
						|
	const auto userpicKey = options.hideNameAndPhoto
 | 
						|
		? InMemoryKey()
 | 
						|
		: peer->userpicUniqueKey(userpicView);
 | 
						|
	const auto userpicPath = _cachedUserpics.get(
 | 
						|
		userpicKey,
 | 
						|
		peer,
 | 
						|
		userpicView);
 | 
						|
	const auto userpicPathWide = QDir::toNativeSeparators(
 | 
						|
		userpicPath).toStdWString();
 | 
						|
	if (modern && !options.hideReplyButton) {
 | 
						|
		SetReplyIconSrc(toastXml, ensureSendButtonIcon());
 | 
						|
		SetReplyPlaceholder(
 | 
						|
			toastXml,
 | 
						|
			tr::lng_message_ph(tr::now).toStdWString());
 | 
						|
	}
 | 
						|
	if (modern && !options.hideMarkAsRead) {
 | 
						|
		SetMarkAsReadText(
 | 
						|
			toastXml,
 | 
						|
			tr::lng_context_mark_read(tr::now).toStdWString());
 | 
						|
	}
 | 
						|
 | 
						|
	SetImageSrc(toastXml, userpicPathWide);
 | 
						|
 | 
						|
	const auto nodeList = toastXml.GetElementsByTagName(L"text");
 | 
						|
	if (nodeList.Length() < (withSubtitle ? 3U : 2U)) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	SetNodeValueString(toastXml, nodeList.Item(0), title.toStdWString());
 | 
						|
	if (withSubtitle) {
 | 
						|
		SetNodeValueString(
 | 
						|
			toastXml,
 | 
						|
			nodeList.Item(1),
 | 
						|
			subtitle.toStdWString());
 | 
						|
	}
 | 
						|
	SetNodeValueString(
 | 
						|
		toastXml,
 | 
						|
		nodeList.Item(withSubtitle ? 2 : 1),
 | 
						|
		msg.toStdWString());
 | 
						|
 | 
						|
	const auto weak = std::weak_ptr(_guarded);
 | 
						|
	const auto performOnMainQueue = [=](FnMut<void(Manager *manager)> task) {
 | 
						|
		crl::on_main(weak, [=, task = std::move(task)]() mutable {
 | 
						|
			task(*weak.lock());
 | 
						|
		});
 | 
						|
	};
 | 
						|
 | 
						|
	auto toast = ToastNotification(toastXml);
 | 
						|
	const auto token1 = toast.Activated([=](
 | 
						|
			const ToastNotification &sender,
 | 
						|
			const winrt::Windows::Foundation::IInspectable &object) {
 | 
						|
		auto activation = ToastActivation();
 | 
						|
		const auto string = &ToastActivation::String;
 | 
						|
		if (const auto args = object.try_as<ToastActivatedEventArgs>()) {
 | 
						|
			activation.args = string(args.Arguments().c_str());
 | 
						|
			const auto args2 = args.try_as<IToastActivatedEventArgs2>();
 | 
						|
			if (!args2 && activation.args.startsWith("action=reply&")) {
 | 
						|
				LOG(("WinRT Error: "
 | 
						|
					"FastReply without IToastActivatedEventArgs2 support."));
 | 
						|
				return;
 | 
						|
			}
 | 
						|
			const auto input = args2 ? args2.UserInput() : nullptr;
 | 
						|
			const auto reply = input
 | 
						|
				? input.TryLookup(L"fastReply")
 | 
						|
				: nullptr;
 | 
						|
			const auto data = reply
 | 
						|
				? reply.try_as<IReference<winrt::hstring>>()
 | 
						|
				: nullptr;
 | 
						|
			if (data) {
 | 
						|
				activation.input.push_back({
 | 
						|
					.key = u"fastReply"_q,
 | 
						|
					.value = string(data.GetString().c_str()),
 | 
						|
				});
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			activation.args = "action=open&" + idString;
 | 
						|
		}
 | 
						|
		crl::on_main([=, activation = std::move(activation)]() mutable {
 | 
						|
			if (const auto strong = weak.lock()) {
 | 
						|
				(*strong)->handleActivation(activation);
 | 
						|
			}
 | 
						|
		});
 | 
						|
	});
 | 
						|
	const auto token2 = toast.Dismissed([=](
 | 
						|
			const ToastNotification &sender,
 | 
						|
			const ToastDismissedEventArgs &args) {
 | 
						|
		const auto reason = args.Reason();
 | 
						|
		switch (reason) {
 | 
						|
		case ToastDismissalReason::ApplicationHidden:
 | 
						|
		case ToastDismissalReason::TimedOut: // Went to Action Center.
 | 
						|
			break;
 | 
						|
		case ToastDismissalReason::UserCanceled:
 | 
						|
		default:
 | 
						|
			performOnMainQueue([notificationId](Manager *manager) {
 | 
						|
				manager->clearNotification(notificationId);
 | 
						|
			});
 | 
						|
			break;
 | 
						|
		}
 | 
						|
	});
 | 
						|
	const auto token3 = toast.Failed([=](
 | 
						|
			const ToastNotification &sender,
 | 
						|
			const ToastFailedEventArgs &args) {
 | 
						|
		performOnMainQueue([notificationId](Manager *manager) {
 | 
						|
			manager->clearNotification(notificationId);
 | 
						|
		});
 | 
						|
	});
 | 
						|
 | 
						|
	auto i = _notifications.find(key);
 | 
						|
	if (i != _notifications.cend()) {
 | 
						|
		auto j = i->second.find(msgId);
 | 
						|
		if (j != i->second.end()) {
 | 
						|
			const auto existing = j->second;
 | 
						|
			i->second.erase(j);
 | 
						|
			tryHide(existing);
 | 
						|
			i = _notifications.find(key);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (i == _notifications.cend()) {
 | 
						|
		i = _notifications.emplace(
 | 
						|
			key,
 | 
						|
			base::flat_map<MsgId, ToastNotification>()).first;
 | 
						|
	}
 | 
						|
	if (!base::WinRT::Try([&] { _notifier.Show(toast); })) {
 | 
						|
		i = _notifications.find(key);
 | 
						|
		if (i != _notifications.cend() && i->second.empty()) {
 | 
						|
			_notifications.erase(i);
 | 
						|
		}
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	i->second.emplace(msgId, toast);
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
void Manager::Private::tryHide(const ToastNotification ¬ification) {
 | 
						|
	base::WinRT::Try([&] {
 | 
						|
		_notifier.Hide(notification);
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
Manager::Manager(Window::Notifications::System *system)
 | 
						|
: NativeManager(system)
 | 
						|
, _private(std::make_unique<Private>(this)) {
 | 
						|
}
 | 
						|
 | 
						|
bool Manager::init() {
 | 
						|
	return _private->init();
 | 
						|
}
 | 
						|
 | 
						|
void Manager::clearNotification(NotificationId id) {
 | 
						|
	_private->clearNotification(id);
 | 
						|
}
 | 
						|
 | 
						|
void Manager::handleActivation(const ToastActivation &activation) {
 | 
						|
	_private->handleActivation(activation);
 | 
						|
}
 | 
						|
 | 
						|
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);
 | 
						|
}
 | 
						|
 | 
						|
void Manager::onBeforeNotificationActivated(NotificationId id) {
 | 
						|
	_private->beforeNotificationActivated(id);
 | 
						|
}
 | 
						|
 | 
						|
void Manager::onAfterNotificationActivated(
 | 
						|
		NotificationId id,
 | 
						|
		not_null<Window::SessionController*> window) {
 | 
						|
	_private->afterNotificationActivated(id, window);
 | 
						|
}
 | 
						|
 | 
						|
bool Manager::doSkipToast() const {
 | 
						|
	return false;
 | 
						|
}
 | 
						|
 | 
						|
void Manager::doMaybePlaySound(Fn<void()> playSound) {
 | 
						|
	const auto skip = SkipSoundForCustom()
 | 
						|
		|| QuietHoursEnabled
 | 
						|
		|| FocusAssistBlocks;
 | 
						|
	if (!skip) {
 | 
						|
		playSound();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
 | 
						|
	const auto skip = SkipFlashBounceForCustom()
 | 
						|
		|| QuietHoursEnabled
 | 
						|
		|| FocusAssistBlocks;
 | 
						|
	if (!skip) {
 | 
						|
		flashBounce();
 | 
						|
	}
 | 
						|
}
 | 
						|
#endif // !__MINGW32__
 | 
						|
 | 
						|
} // namespace Notifications
 | 
						|
} // namespace Platform
 |