diff --git a/Telegram/Resources/lang.strings b/Telegram/Resources/lang.strings index b462bcd6d..19528d4ac 100644 --- a/Telegram/Resources/lang.strings +++ b/Telegram/Resources/lang.strings @@ -185,6 +185,7 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org "lng_settings_desktop_notify" = "Desktop notifications"; "lng_settings_show_name" = "Show sender's name"; "lng_settings_show_preview" = "Show message preview"; +"lng_settings_use_windows" = "Use Windows notifications"; "lng_settings_sound_notify" = "Play sound"; "lng_notification_preview" = "You have a new message"; diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h index fba2537d3..b53fe420e 100644 --- a/Telegram/SourceFiles/config.h +++ b/Telegram/SourceFiles/config.h @@ -321,6 +321,7 @@ enum { MemoryForImageCache = 64 * 1024 * 1024, // after 64mb of unpacked images we try to clear some memory NotifyWindowsCount = 3, // 3 desktop notifies at the same time NotifySettingSaveTimeout = 1000, // wait 1 second before saving notify setting to server + NotifyDeletePhotoAfter = 60000, // delete notify photo after 1 minute UpdateChunk = 100 * 1024, // 100kb parts when downloading the update IdleMsecs = 60 * 1000, // after 60secs without user input we think we are idle diff --git a/Telegram/SourceFiles/gui/images.h b/Telegram/SourceFiles/gui/images.h index 8ebec1dc2..2d1e4a139 100644 --- a/Telegram/SourceFiles/gui/images.h +++ b/Telegram/SourceFiles/gui/images.h @@ -154,6 +154,10 @@ inline StorageKey storageKey(int32 dc, const uint64 &volume, int32 local) { inline StorageKey storageKey(const MTPDfileLocation &location) { return storageKey(location.vdc_id.v, location.vvolume_id.v, location.vlocal_id.v); } +inline StorageKey storageKey(const StorageImageLocation &location) { + return storageKey(location.dc, location.volume, location.local); +} + enum StorageFileType { StorageFileUnknown = 0xaa963b05, // mtpc_storage_fileUnknown StorageFileJpeg = 0x7efe0e, // mtpc_storage_fileJpeg diff --git a/Telegram/SourceFiles/localstorage.cpp b/Telegram/SourceFiles/localstorage.cpp index 4f6103da4..0809f9205 100644 --- a/Telegram/SourceFiles/localstorage.cpp +++ b/Telegram/SourceFiles/localstorage.cpp @@ -19,6 +19,7 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org #include "localstorage.h" #include "mainwidget.h" +#include "window.h" #include "lang.h" namespace { @@ -737,6 +738,15 @@ namespace { cSetDesktopNotify(v == 1); } break; + case dbiWindowsNotifications: { + qint32 v; + stream >> v; + if (!_checkStreamStatus(stream)) return false; + + cSetWindowsNotifications(v == 1); + cSetCustomNotifies((App::wnd() ? App::wnd()->psHasNativeNotifications() : true) && !cWindowsNotifications()); + } break; + case dbiWorkMode: { qint32 v; stream >> v; @@ -1269,7 +1279,7 @@ namespace { _writeMap(WriteMapFast); } - uint32 size = 12 * (sizeof(quint32) + sizeof(qint32)); + uint32 size = 13 * (sizeof(quint32) + sizeof(qint32)); size += sizeof(quint32) + _stringSize(cAskDownloadPath() ? QString() : cDownloadPath()); size += sizeof(quint32) + sizeof(qint32) + (cRecentEmojisPreload().isEmpty() ? cGetRecentEmojis().size() : cRecentEmojisPreload().size()) * (sizeof(uint64) + sizeof(ushort)); size += sizeof(quint32) + sizeof(qint32) + cEmojiVariants().size() * (sizeof(uint32) + sizeof(uint64)); @@ -1285,6 +1295,7 @@ namespace { data.stream << quint32(dbiSoundNotify) << qint32(cSoundNotify()); data.stream << quint32(dbiDesktopNotify) << qint32(cDesktopNotify()); data.stream << quint32(dbiNotifyView) << qint32(cNotifyView()); + data.stream << quint32(dbiWindowsNotifications) << qint32(cWindowsNotifications()); data.stream << quint32(dbiAskDownloadPath) << qint32(cAskDownloadPath()); data.stream << quint32(dbiDownloadPath) << (cAskDownloadPath() ? QString() : cDownloadPath()); data.stream << quint32(dbiCompressPastedImage) << qint32(cCompressPastedImage()); diff --git a/Telegram/SourceFiles/pspecific_linux.h b/Telegram/SourceFiles/pspecific_linux.h index 580fefcec..a4de7f9ba 100644 --- a/Telegram/SourceFiles/pspecific_linux.h +++ b/Telegram/SourceFiles/pspecific_linux.h @@ -68,6 +68,10 @@ public: void psUpdateCounter(); + bool psHasNativeNotifications() { + return false; + } + virtual QImage iconWithCounter(int size, int count, style::color bg, bool smallIcon) = 0; ~PsMainWindow(); diff --git a/Telegram/SourceFiles/pspecific_mac.h b/Telegram/SourceFiles/pspecific_mac.h index 5dcda2877..a9807a175 100644 --- a/Telegram/SourceFiles/pspecific_mac.h +++ b/Telegram/SourceFiles/pspecific_mac.h @@ -83,6 +83,10 @@ public: void psUpdateCounter(); + bool psHasNativeNotifications() { + return !(QSysInfo::macVersion() < QSysInfo::MV_10_8); + } + virtual QImage iconWithCounter(int size, int count, style::color bg, bool smallIcon) = 0; ~PsMainWindow(); diff --git a/Telegram/SourceFiles/pspecific_wnd.cpp b/Telegram/SourceFiles/pspecific_wnd.cpp index a7ad89a93..d8dd244c7 100644 --- a/Telegram/SourceFiles/pspecific_wnd.cpp +++ b/Telegram/SourceFiles/pspecific_wnd.cpp @@ -24,6 +24,8 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org #include "localstorage.h" +#include "passcodewidget.h" + #include #include #include @@ -33,11 +35,29 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org #include #include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + #include #define min(a, b) ((a) < (b) ? (a) : (b)) #define max(a, b) ((a) < (b) ? (b) : (a)) +#include + #ifndef DCX_USESTYLE #define DCX_USESTYLE 0x00010000 #endif @@ -48,9 +68,15 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org #define WM_NCPOINTERUP 0x0243 #endif -#include -#pragma comment (lib,"Gdiplus.lib") -#pragma comment (lib,"Msimg32.lib") +const WCHAR AppUserModelId[] = L"Telegram.TelegramDesktop"; + +static const PROPERTYKEY pkey_AppUserModel_ID = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 5 }; +static const PROPERTYKEY pkey_AppUserModel_StartPinOption = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 12 }; + +using namespace Microsoft::WRL; +using namespace ABI::Windows::UI::Notifications; +using namespace ABI::Windows::Data::Xml::Dom; +using namespace Windows::Foundation; namespace { QStringList _initLogs; @@ -61,6 +87,7 @@ namespace { bool useOpenAs = false; bool useWtsapi = false; bool useShellapi = false; + bool useToast = false; bool themeInited = false; bool finished = true; int menuShown = 0, menuHidden = 0; @@ -70,7 +97,27 @@ namespace { bool sessionLoggedOff = false; UINT tbCreatedMsgId = 0; - ITaskbarList3 *tbListInterface = 0; + + ComPtr taskbarList; + + ComPtr toastNotificationManager; + ComPtr toastNotifier; + ComPtr toastNotificationFactory; + struct ToastNotificationPtr { + ToastNotificationPtr() { + } + ToastNotificationPtr(const ComPtr &ptr) : p(ptr) { + } + ComPtr p; + }; + typedef QMap > ToastNotifications; + ToastNotifications toastNotifications; + struct ToastImage { + uint64 until; + QString path; + }; + typedef QMap ToastImages; + ToastImages toastImages; HWND createTaskbarHider() { HINSTANCE appinst = (HINSTANCE)GetModuleHandle(0); @@ -245,9 +292,7 @@ namespace { destroy(); return false; } -// if (QSysInfo::windowsVersion() >= QSysInfo::WV_WINDOWS8) { - SetWindowLong(hwnds[i], GWL_HWNDPARENT, (LONG)hwnd); -// } + SetWindowLong(hwnds[i], GWL_HWNDPARENT, (LONG)hwnd); dcs[i] = CreateCompatibleDC(screenDC); if (!dcs[i]) { @@ -629,9 +674,6 @@ namespace { typedef HRESULT (FAR STDAPICALLTYPE *f_shCreateItemFromParsingName)(PCWSTR pszPath, IBindCtx *pbc, REFIID riid, void **ppv); f_shCreateItemFromParsingName shCreateItemFromParsingName = 0; - typedef HRESULT (FAR STDAPICALLTYPE *f_shLoadIndirectString)(LPCWSTR pszSource, LPWSTR pszOutBuf, UINT cchOutBuf, void **ppvReserved); - f_shLoadIndirectString shLoadIndirectString = 0; - typedef BOOL (FAR STDAPICALLTYPE *f_wtsRegisterSessionNotification)(HWND hWnd, DWORD dwFlags); f_wtsRegisterSessionNotification wtsRegisterSessionNotification = 0; @@ -640,6 +682,21 @@ namespace { typedef HRESULT (FAR STDAPICALLTYPE *f_shQueryUserNotificationState)(QUERY_USER_NOTIFICATION_STATE *pquns); f_shQueryUserNotificationState shQueryUserNotificationState = 0; + + typedef HRESULT (FAR STDAPICALLTYPE *f_setCurrentProcessExplicitAppUserModelID)(__in PCWSTR AppID); + f_setCurrentProcessExplicitAppUserModelID setCurrentProcessExplicitAppUserModelID = 0; + + typedef HRESULT (FAR STDAPICALLTYPE *f_roGetActivationFactory)(_In_ HSTRING activatableClassId, _In_ REFIID iid, _COM_Outptr_ void ** factory); + f_roGetActivationFactory roGetActivationFactory = 0; + + typedef HRESULT (FAR STDAPICALLTYPE *f_windowsCreateStringReference)(_In_reads_opt_(length + 1) PCWSTR sourceString, UINT32 length, _Out_ HSTRING_HEADER * hstringHeader, _Outptr_result_maybenull_ _Result_nullonfailure_ HSTRING * string); + f_windowsCreateStringReference windowsCreateStringReference = 0; + + typedef HRESULT (FAR STDAPICALLTYPE *f_windowsDeleteString)(_In_opt_ HSTRING string); + f_windowsDeleteString windowsDeleteString = 0; + + typedef HRESULT (FAR STDAPICALLTYPE *f_propVariantToString)(_In_ REFPROPVARIANT propvar, _Out_writes_(cch) PWSTR psz, _In_ UINT cch); + f_propVariantToString propVariantToString = 0; template bool loadFunction(HINSTANCE dll, LPCSTR name, TFunction &func) { @@ -657,6 +714,7 @@ namespace { setupUx(); setupShell(); setupWtsapi(); + setupCombase(); } void setupUx() { HINSTANCE procId = LoadLibrary(L"UXTHEME.DLL"); @@ -669,19 +727,24 @@ namespace { setupOpenWith(procId); setupOpenAs(procId); setupShellapi(procId); + setupAppUserModel(procId); } void setupOpenWith(HINSTANCE procId) { if (!loadFunction(procId, "SHAssocEnumHandlers", shAssocEnumHandlers)) return; if (!loadFunction(procId, "SHCreateItemFromParsingName", shCreateItemFromParsingName)) return; useOpenWith = true; - - HINSTANCE otherProcId = LoadLibrary(L"SHLWAPI.DLL"); - if (otherProcId) loadFunction(otherProcId, "SHLoadIndirectString", shLoadIndirectString); } void setupOpenAs(HINSTANCE procId) { if (!loadFunction(procId, "SHOpenWithDialog", shOpenWithDialog) && !loadFunction(procId, "OpenAs_RunDLLW", openAs_RunDLL)) return; useOpenAs = true; } + void setupShellapi(HINSTANCE procId) { + if (!loadFunction(procId, "SHQueryUserNotificationState", shQueryUserNotificationState)) return; + useShellapi = true; + } + void setupAppUserModel(HINSTANCE procId) { + if (!loadFunction(procId, "SetCurrentProcessExplicitAppUserModelID", setCurrentProcessExplicitAppUserModelID)) return; + } void setupWtsapi() { HINSTANCE procId = LoadLibrary(L"WTSAPI32.DLL"); @@ -689,9 +752,24 @@ namespace { if (!loadFunction(procId, "WTSUnRegisterSessionNotification", wtsUnRegisterSessionNotification)) return; useWtsapi = true; } - void setupShellapi(HINSTANCE procId) { - if (!loadFunction(procId, "SHQueryUserNotificationState", shQueryUserNotificationState)) return; - useShellapi = true; + void setupCombase() { + if (!setCurrentProcessExplicitAppUserModelID) return; + + HINSTANCE procId = LoadLibrary(L"COMBASE.DLL"); + setupToast(procId); + } + void setupToast(HINSTANCE procId) { + if (QSysInfo::windowsVersion() < QSysInfo::WV_WINDOWS8) return; + if (!loadFunction(procId, "RoGetActivationFactory", roGetActivationFactory)) return; + + HINSTANCE otherProcId = LoadLibrary(L"api-ms-win-core-winrt-string-l1-1-0.dll"); + if (!loadFunction(otherProcId, "WindowsCreateStringReference", windowsCreateStringReference)) return; + if (!loadFunction(otherProcId, "WindowsDeleteString", windowsDeleteString)) return; + + HINSTANCE otherOtherProcId = LoadLibrary(L"PROPSYS.DLL"); + if (!loadFunction(otherOtherProcId, "PropVariantToString", propVariantToString)) return; + + useToast = true; } }; _PsInitializer _psInitializer; @@ -718,8 +796,9 @@ namespace { bool mainWindowEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, LRESULT *result) { if (tbCreatedMsgId && msg == tbCreatedMsgId) { - if (CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_ALL, IID_ITaskbarList3, (void**)&tbListInterface) != S_OK) { - tbListInterface = 0; + HRESULT hr = CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&taskbarList)); + if (!SUCCEEDED(hr)) { + taskbarList.Reset(); } } switch (msg) { @@ -915,12 +994,41 @@ namespace { PsMainWindow::PsMainWindow(QWidget *parent) : QMainWindow(parent), ps_hWnd(0), ps_menu(0), icon256(qsl(":/gui/art/icon256.png")), iconbig256(qsl(":/gui/art/iconbig256.png")), wndIcon(QPixmap::fromImage(icon256, Qt::ColorOnly)), ps_iconBig(0), ps_iconSmall(0), ps_iconOverlay(0), trayIcon(0), trayIconMenu(0), posInited(false), ps_tbHider_hWnd(createTaskbarHider()) { tbCreatedMsgId = RegisterWindowMessage(L"TaskbarButtonCreated"); + connect(&ps_cleanNotifyPhotosTimer, SIGNAL(timeout()), this, SLOT(psCleanNotifyPhotos())); } void PsMainWindow::psShowTrayMenu() { trayIconMenu->popup(QCursor::pos()); } +void PsMainWindow::psCleanNotifyPhotosIn(int32 dt) { + if (dt < 0) { + if (ps_cleanNotifyPhotosTimer.isActive() && ps_cleanNotifyPhotosTimer.remainingTime() <= -dt) return; + dt = -dt; + } + ps_cleanNotifyPhotosTimer.start(dt); +} + +void PsMainWindow::psCleanNotifyPhotos() { + uint64 ms = getms(true), minuntil = 0; + for (ToastImages::iterator i = toastImages.begin(); i != toastImages.end();) { + if (!i->until) { + ++i; + continue; + } + if (i->until <= ms) { + QFile(i->path).remove(); + i = toastImages.erase(i); + } else { + if (!minuntil || minuntil > i->until) { + minuntil = i->until; + } + ++i; + } + } + if (minuntil) psCleanNotifyPhotosIn(int32(minuntil - ms)); +} + void PsMainWindow::psRefreshTaskbarIcon() { QWidget *w = new QWidget(this); w->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); @@ -1008,8 +1116,8 @@ void PsMainWindow::psUpdateCounter() { QIcon iconSmall, iconBig; iconSmall.addPixmap(QPixmap::fromImage(iconWithCounter(16, counter, bg, true), Qt::ColorOnly)); iconSmall.addPixmap(QPixmap::fromImage(iconWithCounter(32, counter, bg, true), Qt::ColorOnly)); - iconBig.addPixmap(QPixmap::fromImage(iconWithCounter(32, tbListInterface ? 0 : counter, bg, false), Qt::ColorOnly)); - iconBig.addPixmap(QPixmap::fromImage(iconWithCounter(64, tbListInterface ? 0 : counter, bg, false), Qt::ColorOnly)); + iconBig.addPixmap(QPixmap::fromImage(iconWithCounter(32, taskbarList.Get() ? 0 : counter, bg, false), Qt::ColorOnly)); + iconBig.addPixmap(QPixmap::fromImage(iconWithCounter(64, taskbarList.Get() ? 0 : counter, bg, false), Qt::ColorOnly)); if (trayIcon) { trayIcon->setIcon(iconSmall); } @@ -1020,7 +1128,7 @@ void PsMainWindow::psUpdateCounter() { ps_iconBig = _qt_createHIcon(iconBig, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); SendMessage(ps_hWnd, WM_SETICON, 0, (LPARAM)ps_iconSmall); SendMessage(ps_hWnd, WM_SETICON, 1, (LPARAM)(ps_iconBig ? ps_iconBig : ps_iconSmall)); - if (tbListInterface) { + if (taskbarList.Get()) { if (counter > 0) { QIcon iconOverlay; iconOverlay.addPixmap(QPixmap::fromImage(iconWithCounter(-16, counter, bg, false), Qt::ColorOnly)); @@ -1028,9 +1136,7 @@ void PsMainWindow::psUpdateCounter() { ps_iconOverlay = _qt_createHIcon(iconOverlay, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON)); } QString description = counter > 0 ? QString("%1 unread messages").arg(counter) : qsl("No unread messages"); - static WCHAR descriptionArr[1024]; - description.toWCharArray(descriptionArr); - tbListInterface->SetOverlayIcon(ps_hWnd, ps_iconOverlay, descriptionArr); + taskbarList->SetOverlayIcon(ps_hWnd, ps_iconOverlay, description.toStdWString().c_str()); } SetWindowPos(ps_hWnd, 0, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } @@ -1089,6 +1195,10 @@ void PsMainWindow::psInitSize() { setGeometry(geom); } +bool InitToastManager(); +bool CreateToast(PeerData *peer, int32 msgId, bool showpix, const QString &title, const QString &subtitle, const QString &msg); +void CleanupAppUserModelIdShortcut(); + void PsMainWindow::psInitFrameless() { psUpdatedPositionTimer.setSingleShot(true); connect(&psUpdatedPositionTimer, SIGNAL(timeout()), this, SLOT(psSavePosition())); @@ -1105,6 +1215,9 @@ void PsMainWindow::psInitFrameless() { } // RegisterApplicationRestart(NULL, 0); + if (!InitToastManager()) { + useToast = false; + } psInitSysMenu(); } @@ -1151,8 +1264,18 @@ void PsMainWindow::psUpdatedPosition() { psUpdatedPositionTimer.start(SaveWindowPositionTimeout); } +bool PsMainWindow::psHasNativeNotifications() { + return useToast; +} + Q_DECLARE_METATYPE(QMargins); void PsMainWindow::psFirstShow() { + if (useToast) { + cSetCustomNotifies(!cWindowsNotifications()); + } else { + cSetCustomNotifies(true); + } + _psShadowWindows.init(_shActive); finished = false; @@ -1328,6 +1451,13 @@ PsMainWindow::~PsMainWindow() { } } + if (taskbarList) taskbarList.Reset(); + + toastNotifications.clear(); + if (toastNotificationManager) toastNotificationManager.Reset(); + if (toastNotifier) toastNotifier.Reset(); + if (toastNotificationFactory) toastNotificationFactory.Reset(); + finished = true; if (ps_menu) DestroyMenu(ps_menu); psDestroyIcons(); @@ -1367,12 +1497,40 @@ void PsMainWindow::psActivateNotify(NotifyWindow *w) { } void PsMainWindow::psClearNotifies(PeerId peerId) { + if (!toastNotifier) return; + + if (peerId) { + ToastNotifications::iterator i = toastNotifications.find(peerId); + if (i != toastNotifications.cend()) { + QMap temp = i.value(); + toastNotifications.erase(i); + + for (QMap::const_iterator j = temp.cbegin(), e = temp.cend(); j != e; ++j) { + toastNotifier->Hide(j->p.Get()); + } + } + } else { + ToastNotifications temp = toastNotifications; + toastNotifications.clear(); + + for (ToastNotifications::const_iterator i = temp.cbegin(), end = temp.cend(); i != end; ++i) { + for (QMap::const_iterator j = i->cbegin(), e = i->cend(); j != e; ++j) { + toastNotifier->Hide(j->p.Get()); + } + } + } } void PsMainWindow::psNotifyShown(NotifyWindow *w) { } void PsMainWindow::psPlatformNotify(HistoryItem *item, int32 fwdCount) { + QString title = (!App::passcoded() && cNotifyView() <= dbinvShowName) ? item->history()->peer->name : qsl("Telegram Desktop"); + QString subtitle = (!App::passcoded() && cNotifyView() <= dbinvShowName) ? item->notificationHeader() : QString(); + bool showpix = (!App::passcoded() && cNotifyView() <= dbinvShowName); + QString msg = (!App::passcoded() && cNotifyView() <= dbinvShowPreview) ? (fwdCount < 2 ? item->notificationText() : lng_forward_messages(lt_count, fwdCount)) : lang(lng_notification_preview); + + CreateToast(item->history()->peer, item->id, showpix, title, subtitle, msg); } PsApplication::PsApplication(int &argc, char **argv) : QApplication(argc, argv) { @@ -1704,6 +1862,7 @@ void psDoCleanup() { try { psAutoStart(false, true); psSendToMenu(false, true); + CleanupAppUserModelIdShortcut(); } catch (...) { } } @@ -1822,7 +1981,7 @@ namespace { HBITMAP _iconToBitmap(LPWSTR icon, int iconindex) { if (!icon) return 0; WCHAR tmpIcon[4096]; - if (icon[0] == L'@' && shLoadIndirectString && SUCCEEDED(shLoadIndirectString(icon, tmpIcon, 4096, 0))) { + if (icon[0] == L'@' && SUCCEEDED(SHLoadIndirectString(icon, tmpIcon, 4096, 0))) { icon = tmpIcon; } int32 w = GetSystemMetrics(SM_CXSMICON), h = GetSystemMetrics(SM_CYSMICON); @@ -1983,6 +2142,7 @@ void psStart() { } void psFinish() { + psDeleteDir(cWorkingDir() + qsl("tdata/temp")); } namespace { @@ -2038,10 +2198,12 @@ namespace { } void psRegisterCustomScheme() { + + DEBUG_LOG(("App Info: Checking custom scheme 'tg'..")); HKEY rkey; - QString exe = QDir::toNativeSeparators(QDir(cExeDir()).absolutePath() + '/' + QString::fromWCharArray(AppFile) + qsl(".exe")); + QString exe = QDir::toNativeSeparators(cExeDir() + cExeName()); if (!_psOpenRegKey(L"Software\\Classes\\tg", &rkey)) return; if (!_psSetKeyValue(rkey, L"URL Protocol", QString())) return; @@ -2084,9 +2246,9 @@ void psExecTelegram() { if (cTestMode()) targs += qsl(" -testmode"); if (cDataFile() != qsl("data")) targs += qsl(" -key \"") + cDataFile() + '"'; - QString telegram(QDir::toNativeSeparators(cExeDir() + QString::fromWCharArray(AppFile) + qsl(".exe"))), wdir(QDir::toNativeSeparators(cWorkingDir())); + QString telegram(QDir::toNativeSeparators(cExeDir() + cExeName())), wdir(QDir::toNativeSeparators(cWorkingDir())); - DEBUG_LOG(("Application Info: executing %1 %2").arg(cExeDir() + QString::fromWCharArray(AppFile) + qsl(".exe")).arg(targs)); + DEBUG_LOG(("Application Info: executing %1 %2").arg(cExeDir() + cExeName()).arg(targs)); HINSTANCE r = ShellExecute(0, 0, telegram.toStdWString().c_str(), targs.toStdWString().c_str(), wdir.isEmpty() ? 0 : wdir.toStdWString().c_str(), SW_SHOWNORMAL); if (long(r) < 32) { DEBUG_LOG(("Application Error: failed to execute %1, working directory: '%2', result: %3").arg(telegram).arg(wdir).arg(long(r))); @@ -2095,38 +2257,49 @@ void psExecTelegram() { void _manageAppLnk(bool create, bool silent, int path_csidl, const wchar_t *args, const wchar_t *description) { WCHAR startupFolder[MAX_PATH]; - HRESULT hres = SHGetFolderPath(0, path_csidl, 0, SHGFP_TYPE_CURRENT, startupFolder); - if (SUCCEEDED(hres)) { + HRESULT hr = SHGetFolderPath(0, path_csidl, 0, SHGFP_TYPE_CURRENT, startupFolder); + if (SUCCEEDED(hr)) { QString lnk = QString::fromWCharArray(startupFolder) + '\\' + QString::fromWCharArray(AppFile) + qsl(".lnk"); if (create) { - IShellLink* psl; - hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl); - if (SUCCEEDED(hres)) { - IPersistFile* ppf; + ComPtr shellLink; + hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (SUCCEEDED(hr)) { + ComPtr persistFile; - QString exe = QDir::toNativeSeparators(QDir(cExeDir()).absolutePath() + '/' + QString::fromWCharArray(AppFile) + qsl(".exe")), dir = QDir::toNativeSeparators(QDir(cWorkingDir()).absolutePath()); - psl->SetArguments(args); - psl->SetPath(exe.toStdWString().c_str()); - psl->SetWorkingDirectory(dir.toStdWString().c_str()); - psl->SetDescription(description); + QString exe = QDir::toNativeSeparators(cExeDir() + cExeName()), dir = QDir::toNativeSeparators(QDir(cWorkingDir()).absolutePath()); + shellLink->SetArguments(args); + shellLink->SetPath(exe.toStdWString().c_str()); + shellLink->SetWorkingDirectory(dir.toStdWString().c_str()); + shellLink->SetDescription(description); - hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf); - - if (SUCCEEDED(hres)) { - hres = ppf->Save(lnk.toStdWString().c_str(), TRUE); - ppf->Release(); - } else { - if (!silent) LOG(("App Error: could not create interface IID_IPersistFile %1").arg(hres)); + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (SUCCEEDED(hr)) { + PROPVARIANT appIdPropVar; + hr = InitPropVariantFromString(AppUserModelId, &appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->SetValue(pkey_AppUserModel_ID, appIdPropVar); + PropVariantClear(&appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->Commit(); + } + } + } + + hr = shellLink.As(&persistFile); + if (SUCCEEDED(hr)) { + hr = persistFile->Save(lnk.toStdWString().c_str(), TRUE); + } else { + if (!silent) LOG(("App Error: could not create interface IID_IPersistFile %1").arg(hr)); } - psl->Release(); } else { - if (!silent) LOG(("App Error: could not create instance of IID_IShellLink %1").arg(hres)); + if (!silent) LOG(("App Error: could not create instance of IID_IShellLink %1").arg(hr)); } } else { QFile::remove(lnk); } } else { - if (!silent) LOG(("App Error: could not get CSIDL %1 folder %2").arg(path_csidl).arg(hres)); + if (!silent) LOG(("App Error: could not get CSIDL %1 folder %2").arg(path_csidl).arg(hr)); } } @@ -2247,3 +2420,562 @@ LONG CALLBACK _exceptionFilter(EXCEPTION_POINTERS* pExceptionPointers) { return _oldWndExceptionFilter ? (*_oldWndExceptionFilter)(pExceptionPointers) : EXCEPTION_CONTINUE_SEARCH; } #endif + +class StringReferenceWrapper { +public: + + StringReferenceWrapper(_In_reads_(length) PCWSTR stringRef, _In_ UINT32 length) throw() { + HRESULT hr = windowsCreateStringReference(stringRef, length, &_header, &_hstring); + if (!SUCCEEDED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + } + + ~StringReferenceWrapper() { + windowsDeleteString(_hstring); + } + + template + StringReferenceWrapper(_In_reads_(N) wchar_t const (&stringRef)[N]) throw() { + UINT32 length = N - 1; + HRESULT hr = windowsCreateStringReference(stringRef, length, &_header, &_hstring); + if (!SUCCEEDED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + } + + template + StringReferenceWrapper(_In_reads_(_) wchar_t(&stringRef)[_]) throw() { + UINT32 length; + HRESULT hr = SizeTToUInt32(wcslen(stringRef), &length); + if (!SUCCEEDED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + + windowsCreateStringReference(stringRef, length, &_header, &_hstring); + } + + HSTRING Get() const throw() { + return _hstring; + } + +private: + HSTRING _hstring; + HSTRING_HEADER _header; + +}; + +HRESULT SetNodeValueString(_In_ HSTRING inputString, _In_ IXmlNode *node, _In_ IXmlDocument *xml) { + ComPtr inputText; + + HRESULT hr = xml->CreateTextNode(inputString, &inputText); + if (!SUCCEEDED(hr)) return hr; + ComPtr inputTextNode; + + hr = inputText.As(&inputTextNode); + if (!SUCCEEDED(hr)) return hr; + + ComPtr pAppendedChild; + return node->AppendChild(inputTextNode.Get(), &pAppendedChild); +} + +HRESULT SetAudioSilent(_In_ IXmlDocument *toastXml) { + ComPtr nodeList; + HRESULT hr = toastXml->GetElementsByTagName(StringReferenceWrapper(L"audio").Get(), &nodeList); + if (!SUCCEEDED(hr)) return hr; + + ComPtr audioNode; + hr = nodeList->Item(0, &audioNode); + if (!SUCCEEDED(hr)) return hr; + + if (audioNode) { + ComPtr audioElement; + hr = audioNode.As(&audioElement); + if (!SUCCEEDED(hr)) return hr; + + hr = audioElement->SetAttribute(StringReferenceWrapper(L"silent").Get(), StringReferenceWrapper(L"true").Get()); + if (!SUCCEEDED(hr)) return hr; + } else { + ComPtr audioElement; + hr = toastXml->CreateElement(StringReferenceWrapper(L"audio").Get(), &audioElement); + if (!SUCCEEDED(hr)) return hr; + + hr = audioElement->SetAttribute(StringReferenceWrapper(L"silent").Get(), StringReferenceWrapper(L"true").Get()); + if (!SUCCEEDED(hr)) return hr; + + ComPtr audioNode; + hr = audioElement.As(&audioNode); + if (!SUCCEEDED(hr)) return hr; + + ComPtr nodeList; + hr = toastXml->GetElementsByTagName(StringReferenceWrapper(L"toast").Get(), &nodeList); + if (!SUCCEEDED(hr)) return hr; + + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (!SUCCEEDED(hr)) return hr; + + ComPtr appendedNode; + hr = toastNode->AppendChild(audioNode.Get(), &appendedNode); + } + return hr; +} + +HRESULT SetImageSrc(_In_z_ const wchar_t *imagePath, _In_ IXmlDocument *toastXml) { + wchar_t imageSrc[MAX_PATH] = L"file:///"; + HRESULT hr = StringCchCat(imageSrc, ARRAYSIZE(imageSrc), imagePath); + if (!SUCCEEDED(hr)) return hr; + + ComPtr nodeList; + hr = toastXml->GetElementsByTagName(StringReferenceWrapper(L"image").Get(), &nodeList); + if (!SUCCEEDED(hr)) return hr; + + ComPtr imageNode; + hr = nodeList->Item(0, &imageNode); + if (!SUCCEEDED(hr)) return hr; + + ComPtr attributes; + hr = imageNode->get_Attributes(&attributes); + if (!SUCCEEDED(hr)) return hr; + + ComPtr srcAttribute; + hr = attributes->GetNamedItem(StringReferenceWrapper(L"src").Get(), &srcAttribute); + if (!SUCCEEDED(hr)) return hr; + + return SetNodeValueString(StringReferenceWrapper(imageSrc).Get(), srcAttribute.Get(), toastXml); +} + +typedef ABI::Windows::Foundation::ITypedEventHandler DesktopToastActivatedEventHandler; +typedef ABI::Windows::Foundation::ITypedEventHandler DesktopToastDismissedEventHandler; +typedef ABI::Windows::Foundation::ITypedEventHandler DesktopToastFailedEventHandler; + +class ToastEventHandler : public Implements { +public: + ToastEventHandler::ToastEventHandler(uint64 peer, int32 msg) : _ref(1), _peerId(peer), _msgId(msg) { + } + ~ToastEventHandler() { + } + + // DesktopToastActivatedEventHandler + IFACEMETHODIMP Invoke(_In_ IToastNotification *sender, _In_ IInspectable* args) { + ToastNotifications::iterator i = toastNotifications.find(_peerId); + if (i != toastNotifications.cend()) { + i.value().remove(_msgId); + if (i.value().isEmpty()) { + toastNotifications.erase(i); + } + } + if (App::wnd()) { + History *history = App::history(PeerId(_peerId)); + + App::wnd()->showFromTray(); + if (App::passcoded()) { + App::wnd()->passcodeWidget()->setInnerFocus(); + App::wnd()->notifyClear(); + } else { + App::wnd()->hideSettings(); + bool tomsg = history->peer->chat && (_msgId > 0); + if (tomsg) { + HistoryItem *item = App::histItemById(_msgId); + if (!item || !item->notifyByFrom()) { + tomsg = false; + } + } + App::main()->showPeerHistory(history->peer->id, tomsg ? _msgId : ShowAtUnreadMsgId); + App::wnd()->notifyClear(history); + } + SetForegroundWindow(App::wnd()->psHwnd()); + } + return S_OK; + } + + // DesktopToastDismissedEventHandler + IFACEMETHODIMP Invoke(_In_ IToastNotification *sender, _In_ IToastDismissedEventArgs *e) { + ToastDismissalReason tdr; + if (SUCCEEDED(e->get_Reason(&tdr))) { + switch (tdr) { + case ToastDismissalReason_ApplicationHidden: + break; + case ToastDismissalReason_UserCanceled: + case ToastDismissalReason_TimedOut: + default: + ToastNotifications::iterator i = toastNotifications.find(_peerId); + if (i != toastNotifications.cend()) { + i.value().remove(_msgId); + if (i.value().isEmpty()) { + toastNotifications.erase(i); + } + } + break; + } + } + return S_OK; + } + + // DesktopToastFailedEventHandler + IFACEMETHODIMP Invoke(_In_ IToastNotification *sender, _In_ IToastFailedEventArgs *e) { + ToastNotifications::iterator i = toastNotifications.find(_peerId); + if (i != toastNotifications.cend()) { + i.value().remove(_msgId); + if (i.value().isEmpty()) { + toastNotifications.erase(i); + } + } + return S_OK; + } + + // IUnknown + IFACEMETHODIMP_(ULONG) AddRef() { + return InterlockedIncrement(&_ref); + } + + IFACEMETHODIMP_(ULONG) Release() { + ULONG l = InterlockedDecrement(&_ref); + if (l == 0) delete this; + return l; + } + + IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _COM_Outptr_ void **ppv) { + if (IsEqualIID(riid, IID_IUnknown)) + *ppv = static_cast(static_cast(this)); + else if (IsEqualIID(riid, __uuidof(DesktopToastActivatedEventHandler))) + *ppv = static_cast(this); + else if (IsEqualIID(riid, __uuidof(DesktopToastDismissedEventHandler))) + *ppv = static_cast(this); + else if (IsEqualIID(riid, __uuidof(DesktopToastFailedEventHandler))) + *ppv = static_cast(this); + else *ppv = nullptr; + + if (*ppv) { + reinterpret_cast(*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + +private: + ULONG _ref; + uint64 _peerId; + int32 _msgId; +}; + +template +_Check_return_ __inline HRESULT _1_GetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ T** factory) { + return roGetActivationFactory(activatableClassId, IID_INS_ARGS(factory)); +} + +template +inline HRESULT wrap_GetActivationFactory(_In_ HSTRING activatableClassId, _Inout_ Details::ComPtrRef factory) throw() { + return _1_GetActivationFactory(activatableClassId, factory.ReleaseAndGetAddressOf()); +} + +QString toastImage(const StorageKey &key, PeerData *peer) { + uint64 ms = getms(true); + ToastImages::iterator i = toastImages.find(key); + if (i != toastImages.cend()) { + if (i->until) { + i->until = ms + NotifyDeletePhotoAfter; + if (App::wnd()) App::wnd()->psCleanNotifyPhotosIn(-NotifyDeletePhotoAfter); + } + } else { + ToastImage v; + if (key.first) { + v.until = ms + NotifyDeletePhotoAfter; + if (App::wnd()) App::wnd()->psCleanNotifyPhotosIn(-NotifyDeletePhotoAfter); + } else { + v.until = 0; + } + v.path = cWorkingDir() + qsl("tdata/temp/") + QString::number(MTP::nonce(), 16) + qsl(".png"); + if (peer->photo->loaded() && (key.first || key.second)) { + peer->photo->pix().save(v.path, "PNG"); + } else if (!key.first && key.second) { + (peer->chat ? chatDefPhoto : userDefPhoto)(peer->colorIndex)->pix().save(v.path, "PNG"); + } else { + QFile(":/gui/art/iconbig256.png").copy(v.path); + } + i = toastImages.insert(key, v); + } + return i->path; +} + +bool CreateToast(PeerData *peer, int32 msgId, bool showpix, const QString &title, const QString &subtitle, const QString &msg) { + if (!useToast || !toastNotificationManager || !toastNotifier || !toastNotificationFactory) return false; + + ComPtr toastXml; + bool withSubtitle = !subtitle.isEmpty(); + + HRESULT hr = toastNotificationManager->GetTemplateContent(withSubtitle ? ToastTemplateType_ToastImageAndText04 : ToastTemplateType_ToastImageAndText02, &toastXml); + if (!SUCCEEDED(hr)) return false; + + hr = SetAudioSilent(toastXml.Get()); + if (!SUCCEEDED(hr)) return false; + + StorageKey key; + QString imagePath; + if (showpix) { + if (peer->photoLoc.isNull() || !peer->photo->loaded()) { + key = StorageKey(0, (peer->chat ? 0x2000 : 0x1000) | peer->colorIndex); + } else { + key = storageKey(peer->photoLoc); + } + } else { + key = StorageKey(0, 0); + } + QString image = toastImage(key, peer); + std::wstring wimage = QDir::toNativeSeparators(image).toStdWString(); + + hr = SetImageSrc(wimage.c_str(), toastXml.Get()); + if (!SUCCEEDED(hr)) return false; + + ComPtr nodeList; + hr = toastXml->GetElementsByTagName(StringReferenceWrapper(L"text").Get(), &nodeList); + if (!SUCCEEDED(hr)) return false; + + UINT32 nodeListLength; + hr = nodeList->get_Length(&nodeListLength); + if (!SUCCEEDED(hr)) return false; + + if (nodeListLength < (withSubtitle ? 3 : 2)) return false; + + { + ComPtr textNode; + hr = nodeList->Item(0, &textNode); + if (!SUCCEEDED(hr)) return false; + + std::wstring wtitle = title.toStdWString(); + hr = SetNodeValueString(StringReferenceWrapper(wtitle.data(), wtitle.size()).Get(), textNode.Get(), toastXml.Get()); + if (!SUCCEEDED(hr)) return false; + } + if (withSubtitle) { + ComPtr textNode; + hr = nodeList->Item(1, &textNode); + if (!SUCCEEDED(hr)) return false; + + std::wstring wsubtitle = subtitle.toStdWString(); + hr = SetNodeValueString(StringReferenceWrapper(wsubtitle.data(), wsubtitle.size()).Get(), textNode.Get(), toastXml.Get()); + if (!SUCCEEDED(hr)) return false; + } + { + ComPtr textNode; + hr = nodeList->Item(withSubtitle ? 2 : 1, &textNode); + if (!SUCCEEDED(hr)) return false; + + std::wstring wmsg = msg.toStdWString(); + hr = SetNodeValueString(StringReferenceWrapper(wmsg.data(), wmsg.size()).Get(), textNode.Get(), toastXml.Get()); + if (!SUCCEEDED(hr)) return false; + } + + ComPtr toast; + hr = toastNotificationFactory->CreateToastNotification(toastXml.Get(), &toast); + if (!SUCCEEDED(hr)) return false; + + EventRegistrationToken activatedToken, dismissedToken, failedToken; + ComPtr eventHandler(new ToastEventHandler(peer->id, msgId)); + + hr = toast->add_Activated(eventHandler.Get(), &activatedToken); + if (!SUCCEEDED(hr)) return false; + + hr = toast->add_Dismissed(eventHandler.Get(), &dismissedToken); + if (!SUCCEEDED(hr)) return false; + + hr = toast->add_Failed(eventHandler.Get(), &failedToken); + if (!SUCCEEDED(hr)) return false; + + ToastNotifications::iterator i = toastNotifications.find(peer->id); + if (i == toastNotifications.cend()) { + i = toastNotifications.insert(peer->id, QMap()); + } else { + QMap::iterator j = i->find(msgId); + if (j != i->cend()) { + toastNotifier->Hide(j->p.Get()); + i->erase(j); + if (i->isEmpty()) { + toastNotifications.erase(i); + } + } + } + hr = toastNotifier->Show(toast.Get()); + if (!SUCCEEDED(hr)) { + if (i->isEmpty()) toastNotifications.erase(i); + return false; + } + i->insert(msgId, toast); + + return true; +} + +QString systemShortcutPath() { + static const int maxFileLen = MAX_PATH * 10; + WCHAR wstrPath[maxFileLen]; + if (GetEnvironmentVariable(L"APPDATA", wstrPath, maxFileLen)) { + QDir appData(QString::fromStdWString(std::wstring(wstrPath))); + return appData.absolutePath() + qsl("/Microsoft/Windows/Start Menu/Programs/"); + } + return QString(); +} + +void CleanupAppUserModelIdShortcut() { + static const int maxFileLen = MAX_PATH * 10; + + QString path = systemShortcutPath() + qsl("Telegram.lnk"); + std::wstring p = QDir::toNativeSeparators(path).toStdWString(); + + DWORD attributes = GetFileAttributes(p.c_str()); + if (attributes >= 0xFFFFFFF) return; // file does not exist + + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (!SUCCEEDED(hr)) return; + + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (!SUCCEEDED(hr)) return; + + hr = persistFile->Load(p.c_str(), STGM_READWRITE); + if (!SUCCEEDED(hr)) return; + + WCHAR szGotPath[MAX_PATH]; + WIN32_FIND_DATA wfd; + hr = shellLink->GetPath(szGotPath, MAX_PATH, (WIN32_FIND_DATA*)&wfd, SLGP_SHORTPATH); + if (!SUCCEEDED(hr)) return; + + if (QDir::toNativeSeparators(cExeDir() + cExeName()).toStdWString() == szGotPath) { + QFile().remove(path); + } +} + +bool ValidateAppUserModelIdShortcutAt(const QString &path) { + static const int maxFileLen = MAX_PATH * 10; + + std::wstring p = QDir::toNativeSeparators(path).toStdWString(); + + DWORD attributes = GetFileAttributes(p.c_str()); + if (attributes >= 0xFFFFFFF) return false; // file does not exist + + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (!SUCCEEDED(hr)) return false; + + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (!SUCCEEDED(hr)) return false; + + hr = persistFile->Load(p.c_str(), STGM_READWRITE); + if (!SUCCEEDED(hr)) return false; + + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (!SUCCEEDED(hr)) return false; + + PROPVARIANT appIdPropVar; + hr = propertyStore->GetValue(pkey_AppUserModel_ID, &appIdPropVar); + if (!SUCCEEDED(hr)) return false; + + WCHAR already[MAX_PATH]; + hr = propVariantToString(appIdPropVar, already, MAX_PATH); + if (SUCCEEDED(hr)) { + if (std::wstring(AppUserModelId) == already) { + PropVariantClear(&appIdPropVar); + return true; + } + } + if (appIdPropVar.vt != VT_EMPTY) { + PropVariantClear(&appIdPropVar); + return false; + } + PropVariantClear(&appIdPropVar); + + hr = InitPropVariantFromString(AppUserModelId, &appIdPropVar); + if (!SUCCEEDED(hr)) return false; + + hr = propertyStore->SetValue(pkey_AppUserModel_ID, appIdPropVar); + PropVariantClear(&appIdPropVar); + if (!SUCCEEDED(hr)) return false; + + hr = propertyStore->Commit(); + if (!SUCCEEDED(hr)) return false; + + if (persistFile->IsDirty() == S_OK) { + persistFile->Save(p.c_str(), TRUE); + } + + return true; +} + +bool ValidateAppUserModelIdShortcut() { + if (!useToast) return false; + + QString path = systemShortcutPath(); + if (path.isEmpty()) return false; + + if (ValidateAppUserModelIdShortcutAt(path + qsl("Telegram Desktop/Telegram.lnk"))) return true; + if (ValidateAppUserModelIdShortcutAt(path + qsl("Telegram Win (Unofficial)/Telegram.lnk"))) return true; + + path += qsl("Telegram.lnk"); + if (ValidateAppUserModelIdShortcutAt(path)) return true; + + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (!SUCCEEDED(hr)) return false; + + hr = shellLink->SetPath(QDir::toNativeSeparators(cExeDir() + cExeName()).toStdWString().c_str()); + if (!SUCCEEDED(hr)) return false; + + hr = shellLink->SetArguments(L""); + if (!SUCCEEDED(hr)) return false; + + hr = shellLink->SetWorkingDirectory(QDir::toNativeSeparators(QDir(cWorkingDir()).absolutePath()).toStdWString().c_str()); + if (!SUCCEEDED(hr)) return false; + + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (!SUCCEEDED(hr)) return false; + + PROPVARIANT appIdPropVar; + hr = InitPropVariantFromString(AppUserModelId, &appIdPropVar); + if (!SUCCEEDED(hr)) return false; + + hr = propertyStore->SetValue(pkey_AppUserModel_ID, appIdPropVar); + PropVariantClear(&appIdPropVar); + if (!SUCCEEDED(hr)) return false; + + PROPVARIANT startPinPropVar; + hr = InitPropVariantFromUInt32(APPUSERMODEL_STARTPINOPTION_NOPINONINSTALL, &startPinPropVar); + if (!SUCCEEDED(hr)) return false; + + hr = propertyStore->SetValue(pkey_AppUserModel_StartPinOption, startPinPropVar); + PropVariantClear(&startPinPropVar); + if (!SUCCEEDED(hr)) return false; + + hr = propertyStore->Commit(); + if (!SUCCEEDED(hr)) return false; + + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (!SUCCEEDED(hr)) return false; + + hr = persistFile->Save(QDir::toNativeSeparators(path).toStdWString().c_str(), TRUE); + if (!SUCCEEDED(hr)) return false; + + return true; +} + +bool InitToastManager() { + if (!useToast || !ValidateAppUserModelIdShortcut()) return false; + + if (!SUCCEEDED(setCurrentProcessExplicitAppUserModelID(AppUserModelId))) { + return false; + } + if (!SUCCEEDED(wrap_GetActivationFactory(StringReferenceWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), &toastNotificationManager))) { + return false; + } + if (!SUCCEEDED(toastNotificationManager->CreateToastNotifierWithId(StringReferenceWrapper(AppUserModelId).Get(), &toastNotifier))) { + return false; + } + if (!SUCCEEDED(wrap_GetActivationFactory(StringReferenceWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), &toastNotificationFactory))) { + return false; + } + QDir().mkpath(cWorkingDir() + qsl("tdata/temp")); + return true; +} diff --git a/Telegram/SourceFiles/pspecific_wnd.h b/Telegram/SourceFiles/pspecific_wnd.h index 6e8040b23..bf07c1c1a 100644 --- a/Telegram/SourceFiles/pspecific_wnd.h +++ b/Telegram/SourceFiles/pspecific_wnd.h @@ -67,6 +67,9 @@ public: void psUpdateCounter(); + bool psHasNativeNotifications(); + void psCleanNotifyPhotosIn(int32 dt); + virtual QImage iconWithCounter(int size, int count, style::color bg, bool smallIcon) = 0; ~PsMainWindow(); @@ -77,6 +80,8 @@ public slots: void psSavePosition(Qt::WindowState state = Qt::WindowActive); void psShowTrayMenu(); + void psCleanNotifyPhotos(); + protected: bool psHasTrayIcon() const { @@ -100,6 +105,8 @@ private: HMENU ps_menu; HICON ps_iconBig, ps_iconSmall, ps_iconOverlay; + SingleTimer ps_cleanNotifyPhotosTimer; + void psDestroyIcons(); }; diff --git a/Telegram/SourceFiles/settings.cpp b/Telegram/SourceFiles/settings.cpp index a9a61afba..da839b676 100644 --- a/Telegram/SourceFiles/settings.cpp +++ b/Telegram/SourceFiles/settings.cpp @@ -42,6 +42,7 @@ QString gDialogLastPath, gDialogHelperPath; // optimize QFileDialog bool gSoundNotify = true; bool gDesktopNotify = true; DBINotifyView gNotifyView = dbinvShowPreview; +bool gWindowsNotifications = true; bool gStartMinimized = false; bool gStartInTray = false; bool gAutoStart = false; @@ -119,11 +120,7 @@ QString gLangFile; bool gRetina = false; float64 gRetinaFactor = 1.; int32 gIntRetinaFactor = 1; -#ifdef Q_OS_MAC -bool gCustomNotifies = false; -#else bool gCustomNotifies = true; -#endif uint64 gInstance = 0.; #ifdef Q_OS_WIN @@ -163,7 +160,6 @@ SavedPeers gSavedPeers; SavedPeersByTime gSavedPeersByTime; void settingsParseArgs(int argc, char *argv[]) { - gCustomNotifies = true; #ifdef Q_OS_MAC if (QSysInfo::macVersion() < QSysInfo::MV_10_8) { gUpdateURL = QUrl(qsl("http://tdesktop.com/mac32/tupdates/current")); diff --git a/Telegram/SourceFiles/settings.h b/Telegram/SourceFiles/settings.h index f5b577fbf..47c9a3610 100644 --- a/Telegram/SourceFiles/settings.h +++ b/Telegram/SourceFiles/settings.h @@ -101,6 +101,8 @@ DeclareSetting(bool, DesktopNotify); DeclareSetting(DBINotifyView, NotifyView); DeclareSetting(bool, AutoUpdate); +DeclareSetting(bool, WindowsNotifications); + struct TWindowPos { TWindowPos() : moncrc(0), maximized(0), x(0), y(0), w(0), h(0) { } diff --git a/Telegram/SourceFiles/settingswidget.cpp b/Telegram/SourceFiles/settingswidget.cpp index 641eb499d..acda58647 100644 --- a/Telegram/SourceFiles/settingswidget.cpp +++ b/Telegram/SourceFiles/settingswidget.cpp @@ -122,6 +122,7 @@ SettingsInner::SettingsInner(SettingsWidget *parent) : QWidget(parent), _desktopNotify(this, lang(lng_settings_desktop_notify), cDesktopNotify()), _senderName(this, lang(lng_settings_show_name), cNotifyView() <= dbinvShowName), _messagePreview(this, lang(lng_settings_show_preview), cNotifyView() <= dbinvShowPreview), + _windowsNotifications(this, lang(lng_settings_use_windows), cWindowsNotifications()), _soundNotify(this, lang(lng_settings_sound_notify), cSoundNotify()), // general @@ -217,6 +218,7 @@ SettingsInner::SettingsInner(SettingsWidget *parent) : QWidget(parent), connect(&_desktopNotify, SIGNAL(changed()), this, SLOT(onDesktopNotify())); connect(&_senderName, SIGNAL(changed()), this, SLOT(onSenderName())); connect(&_messagePreview, SIGNAL(changed()), this, SLOT(onMessagePreview())); + connect(&_windowsNotifications, SIGNAL(changed()), this, SLOT(onWindowsNotifications())); connect(&_soundNotify, SIGNAL(changed()), this, SLOT(onSoundNotify())); // general @@ -411,6 +413,9 @@ void SettingsInner::paintEvent(QPaintEvent *e) { top += _desktopNotify.height() + st::setLittleSkip; top += _senderName.height() + st::setLittleSkip; top += _messagePreview.height() + st::setSectionSkip; + if (App::wnd()->psHasNativeNotifications() && cPlatform() == dbipWindows) { + top += _windowsNotifications.height() + st::setSectionSkip; + } top += _soundNotify.height(); } @@ -637,6 +642,9 @@ void SettingsInner::resizeEvent(QResizeEvent *e) { _desktopNotify.move(_left, top); top += _desktopNotify.height() + st::setLittleSkip; _senderName.move(_left, top); top += _senderName.height() + st::setLittleSkip; _messagePreview.move(_left, top); top += _messagePreview.height() + st::setSectionSkip; + if (App::wnd()->psHasNativeNotifications() && cPlatform() == dbipWindows) { + _windowsNotifications.move(_left, top); top += _windowsNotifications.height() + st::setSectionSkip; + } _soundNotify.move(_left, top); top += _soundNotify.height(); } @@ -936,11 +944,17 @@ void SettingsInner::showAll() { _desktopNotify.show(); _senderName.show(); _messagePreview.show(); + if (App::wnd()->psHasNativeNotifications() && cPlatform() == dbipWindows) { + _windowsNotifications.show(); + } else { + _windowsNotifications.hide(); + } _soundNotify.show(); } else { _desktopNotify.hide(); _senderName.hide(); _messagePreview.hide(); + _windowsNotifications.hide(); _soundNotify.hide(); } @@ -1349,6 +1363,13 @@ void SettingsInner::onSoundNotify() { Local::writeUserSettings(); } +void SettingsInner::onWindowsNotifications() { + cSetWindowsNotifications(!cWindowsNotifications()); + App::wnd()->notifyClearFast(); + cSetCustomNotifies(!cWindowsNotifications()); + Local::writeUserSettings(); +} + void SettingsInner::onDesktopNotify() { cSetDesktopNotify(_desktopNotify.checked()); if (!_desktopNotify.checked()) { diff --git a/Telegram/SourceFiles/settingswidget.h b/Telegram/SourceFiles/settingswidget.h index 1de46a36c..0ce9dcfa9 100644 --- a/Telegram/SourceFiles/settingswidget.h +++ b/Telegram/SourceFiles/settingswidget.h @@ -125,6 +125,8 @@ public slots: void onSenderName(); void onMessagePreview(); + void onWindowsNotifications(); + void onReplaceEmojis(); void onViewEmojis(); @@ -192,7 +194,7 @@ private: LinkButton _chooseUsername; // notifications - FlatCheckbox _desktopNotify, _senderName, _messagePreview, _soundNotify; + FlatCheckbox _desktopNotify, _senderName, _messagePreview, _windowsNotifications, _soundNotify; // general LinkButton _changeLanguage; diff --git a/Telegram/SourceFiles/types.h b/Telegram/SourceFiles/types.h index 5d8ee7a0e..976f40085 100644 --- a/Telegram/SourceFiles/types.h +++ b/Telegram/SourceFiles/types.h @@ -232,55 +232,56 @@ QString translitRusEng(const QString &rus); QString rusKeyboardLayoutSwitch(const QString &from); enum DataBlockId { - dbiKey = 0x00, - dbiUser = 0x01, - dbiDcOptionOld = 0x02, - dbiMaxGroupCount = 0x03, - dbiMutePeer = 0x04, - dbiSendKey = 0x05, - dbiAutoStart = 0x06, - dbiStartMinimized = 0x07, - dbiSoundNotify = 0x08, - dbiWorkMode = 0x09, - dbiSeenTrayTooltip = 0x0a, - dbiDesktopNotify = 0x0b, - dbiAutoUpdate = 0x0c, - dbiLastUpdateCheck = 0x0d, - dbiWindowPosition = 0x0e, - dbiConnectionType = 0x0f, + dbiKey = 0x00, + dbiUser = 0x01, + dbiDcOptionOld = 0x02, + dbiMaxGroupCount = 0x03, + dbiMutePeer = 0x04, + dbiSendKey = 0x05, + dbiAutoStart = 0x06, + dbiStartMinimized = 0x07, + dbiSoundNotify = 0x08, + dbiWorkMode = 0x09, + dbiSeenTrayTooltip = 0x0a, + dbiDesktopNotify = 0x0b, + dbiAutoUpdate = 0x0c, + dbiLastUpdateCheck = 0x0d, + dbiWindowPosition = 0x0e, + dbiConnectionType = 0x0f, // 0x10 reserved - dbiDefaultAttach = 0x11, - dbiCatsAndDogs = 0x12, - dbiReplaceEmojis = 0x13, - dbiAskDownloadPath = 0x14, - dbiDownloadPath = 0x15, - dbiScale = 0x16, - dbiEmojiTab = 0x17, - dbiRecentEmojisOld = 0x18, - dbiLoggedPhoneNumber = 0x19, - dbiMutedPeers = 0x1a, + dbiDefaultAttach = 0x11, + dbiCatsAndDogs = 0x12, + dbiReplaceEmojis = 0x13, + dbiAskDownloadPath = 0x14, + dbiDownloadPath = 0x15, + dbiScale = 0x16, + dbiEmojiTab = 0x17, + dbiRecentEmojisOld = 0x18, + dbiLoggedPhoneNumber = 0x19, + dbiMutedPeers = 0x1a, // 0x1b reserved - dbiNotifyView = 0x1c, - dbiSendToMenu = 0x1d, - dbiCompressPastedImage = 0x1e, - dbiLang = 0x1f, - dbiLangFile = 0x20, - dbiTileBackground = 0x21, - dbiAutoLock = 0x22, - dbiDialogLastPath = 0x23, - dbiRecentEmojis = 0x24, - dbiEmojiVariants = 0x25, - dbiRecentStickers = 0x26, - dbiDcOption = 0x27, - dbiTryIPv6 = 0x28, - dbiSongVolume = 0x29, + dbiNotifyView = 0x1c, + dbiSendToMenu = 0x1d, + dbiCompressPastedImage = 0x1e, + dbiLang = 0x1f, + dbiLangFile = 0x20, + dbiTileBackground = 0x21, + dbiAutoLock = 0x22, + dbiDialogLastPath = 0x23, + dbiRecentEmojis = 0x24, + dbiEmojiVariants = 0x25, + dbiRecentStickers = 0x26, + dbiDcOption = 0x27, + dbiTryIPv6 = 0x28, + dbiSongVolume = 0x29, + dbiWindowsNotifications = 0x30, - dbiEncryptedWithSalt = 333, - dbiEncrypted = 444, + dbiEncryptedWithSalt = 333, + dbiEncrypted = 444, // 500-600 reserved - dbiVersion = 666, + dbiVersion = 666, }; enum DBISendKey { diff --git a/Telegram/Telegram.vcxproj b/Telegram/Telegram.vcxproj index 80b18b34f..b749011c5 100644 --- a/Telegram/Telegram.vcxproj +++ b/Telegram/Telegram.vcxproj @@ -54,14 +54,17 @@ $(SolutionDir)$(Platform)\$(Configuration)Intermediate\ + $(VC_IncludePath);$(WindowsSDK_IncludePath); $(SolutionDir)$(Platform)\$(Configuration)\ $(SolutionDir)$(Platform)\$(Configuration)Intermediate\ + $(VC_IncludePath);$(WindowsSdk_71A_IncludePath);$(WindowsSDK_IncludePath); $(SolutionDir)$(Platform)\$(Configuration)\ $(SolutionDir)$(Platform)\$(Configuration)Intermediate\ + $(VC_IncludePath);$(WindowsSdk_71A_IncludePath);$(WindowsSDK_IncludePath); @@ -80,7 +83,7 @@ Windows $(OutDir)$(ProjectName).exe .\..\..\Libraries\lzma\C\Util\LzmaLib\Debug;.\..\..\Libraries\libexif-0.6.20\win32\Debug;.\..\..\Libraries\ffmpeg-2.6.3;.\..\..\Libraries\opus\win32\VS2010\Win32\Debug;.\..\..\Libraries\openal-soft\build\Debug;.\..\..\Libraries\zlib-1.2.8\contrib\vstudio\vc11\x86\ZlibStatDebug;.\..\..\Libraries\OpenSSL-Win32\lib\VC\static;$(QTDIR)\lib;$(QTDIR)\plugins;%(AdditionalLibraryDirectories) - kernel32.lib;user32.lib;shell32.lib;uuid.lib;ole32.lib;advapi32.lib;ws2_32.lib;gdi32.lib;comdlg32.lib;oleaut32.lib;imm32.lib;winmm.lib;qtmaind.lib;glu32.lib;opengl32.lib;Strmiids.lib;Qt5Cored.lib;Qt5Guid.lib;qtharfbuzzngd.lib;qtpcred.lib;qtfreetyped.lib;Qt5Widgetsd.lib;Qt5Networkd.lib;Qt5PlatformSupportd.lib;platforms\qwindowsd.lib;imageformats\qwebpd.lib;libeay32MTd.lib;ssleay32MTd.lib;Crypt32.lib;zlibstat.lib;LzmaLib.lib;lib_exif.lib;UxTheme.lib;DbgHelp.lib;OpenAL32.lib;common.lib;libavformat\libavformat.a;libavcodec\libavcodec.a;libavutil\libavutil.a;libswresample\libswresample.a;opus.lib;celt.lib;silk_common.lib;silk_float.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;shell32.lib;uuid.lib;ole32.lib;advapi32.lib;ws2_32.lib;gdi32.lib;comdlg32.lib;oleaut32.lib;Shlwapi.lib;Gdiplus.lib;imm32.lib;winmm.lib;qtmaind.lib;glu32.lib;opengl32.lib;Strmiids.lib;Qt5Cored.lib;Qt5Guid.lib;qtharfbuzzngd.lib;qtpcred.lib;qtfreetyped.lib;Qt5Widgetsd.lib;Qt5Networkd.lib;Qt5PlatformSupportd.lib;platforms\qwindowsd.lib;imageformats\qwebpd.lib;libeay32MTd.lib;ssleay32MTd.lib;Crypt32.lib;zlibstat.lib;LzmaLib.lib;lib_exif.lib;UxTheme.lib;DbgHelp.lib;OpenAL32.lib;common.lib;libavformat\libavformat.a;libavcodec\libavcodec.a;libavutil\libavutil.a;libswresample\libswresample.a;opus.lib;celt.lib;silk_common.lib;silk_float.lib;%(AdditionalDependencies) true LIBCMT