diff --git a/imports/Spectral/Page/Room.qml b/imports/Spectral/Page/Room.qml index 4e05fa0..dbcda44 100644 --- a/imports/Spectral/Page/Room.qml +++ b/imports/Spectral/Page/Room.qml @@ -1,5 +1,5 @@ import QtQuick 2.9 RoomForm { - roomListModel.onNewMessage: if (!window.visible) spectralController.showMessage(roomName, content, icon) + roomListModel.onNewMessage: if (!window.visible) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon) } diff --git a/imports/Spectral/Page/RoomForm.ui.qml b/imports/Spectral/Page/RoomForm.ui.qml index 0c35fd4..59efb3e 100644 --- a/imports/Spectral/Page/RoomForm.ui.qml +++ b/imports/Spectral/Page/RoomForm.ui.qml @@ -15,6 +15,7 @@ Page { property alias filter: roomListForm.filter property alias roomListModel: roomListModel + property alias enteredRoom: roomListForm.enteredRoom id: page diff --git a/imports/Spectral/Panel/RoomPanelInput.qml b/imports/Spectral/Panel/RoomPanelInput.qml index 7daec12..770458e 100644 --- a/imports/Spectral/Panel/RoomPanelInput.qml +++ b/imports/Spectral/Panel/RoomPanelInput.qml @@ -36,6 +36,12 @@ Rectangle { } onClicked: currentRoom.chooseAndUploadFile() + + BusyIndicator { + anchors.fill: parent + + running: false + } } ScrollView { diff --git a/qml/main.qml b/qml/main.qml index eaf1924..b53cae3 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -38,6 +38,10 @@ ApplicationWindow { window.requestActivate() } onHideWindow: window.hide() + onNotificationClicked: { + roomPage.enteredRoom = currentConnection.room(roomId) + showWindow() + } onErrorOccured: { errorDialog.error = error errorDialog.detail = detail diff --git a/spectral.pro b/spectral.pro index 0367427..51a3ef8 100644 --- a/spectral.pro +++ b/spectral.pro @@ -1,4 +1,4 @@ -QT += quick widgets multimedia +QT += quick widgets multimedia dbus CONFIG += c++14 CONFIG += object_parallel_to_source CONFIG += link_pkgconfig @@ -36,18 +36,6 @@ DEFINES += QT_DEPRECATED_WARNINGS # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 -SOURCES += src/main.cpp \ - src/controller.cpp \ - src/roomlistmodel.cpp \ - src/imageprovider.cpp \ - src/messageeventmodel.cpp \ - src/emojimodel.cpp \ - src/spectralroom.cpp \ - src/userlistmodel.cpp \ - src/imageitem.cpp \ - src/accountlistmodel.cpp \ - src/spectraluser.cpp - RESOURCES += \ res.qrc @@ -85,13 +73,40 @@ mac { } HEADERS += \ - $$PWD/src/controller.h \ - $$PWD/src/roomlistmodel.h \ - $$PWD/src/imageprovider.h \ - $$PWD/src/messageeventmodel.h \ - $$PWD/src/emojimodel.h \ - $$PWD/src/spectralroom.h \ - $$PWD/src/userlistmodel.h \ - $$PWD/src/imageitem.h \ - $$PWD/src/accountlistmodel.h \ - $$PWD/src/spectraluser.h + src/controller.h \ + src/roomlistmodel.h \ + src/imageprovider.h \ + src/messageeventmodel.h \ + src/emojimodel.h \ + src/spectralroom.h \ + src/userlistmodel.h \ + src/imageitem.h \ + src/accountlistmodel.h \ + src/spectraluser.h \ + src/notifications/manager.h + +SOURCES += src/main.cpp \ + src/controller.cpp \ + src/roomlistmodel.cpp \ + src/imageprovider.cpp \ + src/messageeventmodel.cpp \ + src/emojimodel.cpp \ + src/spectralroom.cpp \ + src/userlistmodel.cpp \ + src/imageitem.cpp \ + src/accountlistmodel.cpp \ + src/spectraluser.cpp + +unix:!mac { + SOURCES += src/notifications/managerlinux.cpp +} + +win32 { + HEADERS += src/notifications/wintoastlib.h + SOURCES += src/notifications/managerwin.cpp \ + src/notifications/wintoastlib.cpp +} + +mac { + SOURCES += src/notifications/managermac.mm +} diff --git a/src/controller.cpp b/src/controller.cpp index d276fd1..8d83a65 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -27,18 +27,20 @@ #include #include -Controller::Controller(QObject* parent) : QObject(parent) { - tray->setIcon(QIcon(":/assets/img/icon.png")); - tray->setToolTip("Spectral"); - connect(tray, &QSystemTrayIcon::activated, +Controller::Controller(QObject* parent) + : QObject(parent), tray(this), notificationsManager(this) { + connect(¬ificationsManager, &NotificationsManager::notificationClicked, + this, &Controller::notificationClicked); + tray.setIcon(QIcon(":/assets/img/icon.png")); + tray.setToolTip("Spectral"); + connect(&tray, &QSystemTrayIcon::activated, [this](QSystemTrayIcon::ActivationReason r) { if (r != QSystemTrayIcon::Context) emit showWindow(); }); - connect(tray, &QSystemTrayIcon::messageClicked, [=] { emit showWindow(); }); - trayMenu->addAction("Hide Window", [=] { emit hideWindow(); }); - trayMenu->addAction("Quit", [=] { QApplication::quit(); }); - tray->setContextMenu(trayMenu); - tray->show(); + trayMenu.addAction("Hide Window", [=] { emit hideWindow(); }); + trayMenu.addAction("Quit", [=] { QApplication::quit(); }); + tray.setContextMenu(&trayMenu); + tray.show(); Connection::setRoomType(); Connection::setUserType(); @@ -226,11 +228,6 @@ void Controller::playAudio(QUrl localFile) { connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); }); } -void Controller::showMessage(const QString& title, const QString& msg, - const QIcon& icon) { - tray->showMessage(title, msg, icon); -} - QImage Controller::safeImage(QImage image) { if (image.isNull()) return QImage(); return image; @@ -243,3 +240,11 @@ QColor Controller::color(QString userId) { void Controller::setColor(QString userId, QColor newColor) { SettingsGroup("UI/Color").setValue(userId, newColor.name()); } + +void Controller::postNotification(const QString& roomId, const QString& eventId, + const QString& roomName, + const QString& senderName, + const QString& text, const QImage& icon) { + notificationsManager.postNotification(roomId, eventId, roomName, senderName, + text, icon); +} diff --git a/src/controller.h b/src/controller.h index 35d964f..95ff024 100644 --- a/src/controller.h +++ b/src/controller.h @@ -2,6 +2,7 @@ #define CONTROLLER_H #include "connection.h" +#include "notifications/manager.h" #include "settings.h" #include "user.h" @@ -39,8 +40,9 @@ class Controller : public QObject { private: QClipboard* m_clipboard = QApplication::clipboard(); - QSystemTrayIcon* tray = new QSystemTrayIcon(); - QMenu* trayMenu = new QMenu(); + QSystemTrayIcon tray; + QMenu trayMenu; + NotificationsManager notificationsManager; QVector m_connections; QByteArray loadAccessToken(const AccountSettings& account); @@ -60,6 +62,7 @@ class Controller : public QObject { void connectionAdded(Connection* conn); void connectionDropped(Connection* conn); void initiated(); + void notificationClicked(const QString roomId, const QString eventId); public slots: void logout(Connection* conn); @@ -68,7 +71,9 @@ class Controller : public QObject { void createDirectChat(Connection* c, const QString& userID); void copyToClipboard(const QString& text); void playAudio(QUrl localFile); - void showMessage(const QString& title, const QString& msg, const QIcon& icon); + void postNotification(const QString& roomId, const QString& eventId, + const QString& roomName, const QString& senderName, + const QString& text, const QImage& icon); static QImage safeImage(QImage image); }; diff --git a/src/main.cpp b/src/main.cpp index d393139..365321b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -49,8 +49,9 @@ int main(int argc, char *argv[]) { qmlRegisterUncreatableType("Spectral", 0, 1, "RoomType", "ENUM"); qRegisterMetaType("User*"); + qRegisterMetaType("Room*"); qRegisterMetaType("MessageEventType"); - qRegisterMetaType("SpectralRoom"); + qRegisterMetaType("SpectralRoom*"); QQmlApplicationEngine engine; diff --git a/src/notifications/manager.h b/src/notifications/manager.h new file mode 100644 index 0000000..de8ac89 --- /dev/null +++ b/src/notifications/manager.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#include +#include +#endif + +struct roomEventId { + QString roomId; + QString eventId; +}; + +class NotificationsManager : public QObject { + Q_OBJECT + public: + NotificationsManager(QObject *parent = nullptr); + + void postNotification(const QString &roomId, const QString &eventId, + const QString &roomName, const QString &senderName, + const QString &text, const QImage &icon); + + signals: + void notificationClicked(const QString roomId, const QString eventId); + +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + private: + QDBusInterface dbus; + uint showNotification(const QString summary, const QString text, + const QImage image); + + // notification ID to (room ID, event ID) + QMap notificationIds; +#endif + + // these slots are platform specific (D-Bus only) + // but Qt slot declarations can not be inside an ifdef! + private slots: + void actionInvoked(uint id, QString action); + void notificationClosed(uint id, uint reason); +}; + +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +QDBusArgument &operator<<(QDBusArgument &arg, const QImage &image); +const QDBusArgument &operator>>(const QDBusArgument &arg, QImage &); +#endif diff --git a/src/notifications/managerlinux.cpp b/src/notifications/managerlinux.cpp new file mode 100644 index 0000000..368cd02 --- /dev/null +++ b/src/notifications/managerlinux.cpp @@ -0,0 +1,133 @@ +#include "manager.h" + +#include +#include +#include +#include +#include + +NotificationsManager::NotificationsManager(QObject *parent) + : QObject(parent), + dbus("org.freedesktop.Notifications", "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", QDBusConnection::sessionBus(), + this) { + qDBusRegisterMetaType(); + + QDBusConnection::sessionBus().connect( + "org.freedesktop.Notifications", "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", "ActionInvoked", this, + SLOT(actionInvoked(uint, QString))); + QDBusConnection::sessionBus().connect( + "org.freedesktop.Notifications", "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", "NotificationClosed", this, + SLOT(notificationClosed(uint, uint))); +} + +void NotificationsManager::postNotification( + const QString &roomid, const QString &eventid, const QString &roomname, + const QString &sender, const QString &text, const QImage &icon) { + uint id = showNotification(roomname, sender + ": " + text, icon); + notificationIds[id] = roomEventId{roomid, eventid}; +} +/** + * This function is based on code from + * https://github.com/rohieb/StratumsphereTrayIcon + * Copyright (C) 2012 Roland Hieber + * Licensed under the GNU General Public License, version 3 + */ +uint NotificationsManager::showNotification(const QString summary, + const QString text, + const QImage image) { + QVariantMap hints; + hints["image-data"] = image; + QList argumentList; + argumentList << "Spectral"; // app_name + argumentList << uint(0); // replace_id + argumentList << ""; // app_icon + argumentList << summary; // summary + argumentList << text; // body + argumentList << (QStringList("default") << "reply"); // actions + argumentList << hints; // hints + argumentList << int(-1); // timeout in ms + + static QDBusInterface notifyApp("org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications"); + QDBusMessage reply = + notifyApp.callWithArgumentList(QDBus::AutoDetect, "Notify", argumentList); + if (reply.type() == QDBusMessage::ErrorMessage) { + qDebug() << "D-Bus Error:" << reply.errorMessage(); + return 0; + } else { + return reply.arguments().first().toUInt(); + } +} + +void NotificationsManager::actionInvoked(uint id, QString action) { + if (action == "default" && notificationIds.contains(id)) { + roomEventId idEntry = notificationIds[id]; + emit notificationClicked(idEntry.roomId, idEntry.eventId); + } +} + +void NotificationsManager::notificationClosed(uint id, uint reason) { + Q_UNUSED(reason); + notificationIds.remove(id); +} + +/** + * Automatic marshaling of a QImage for org.freedesktop.Notifications.Notify + * + * This function is from the Clementine project (see + * http://www.clementine-player.org) and licensed under the GNU General Public + * License, version 3 or later. + * + * Copyright 2010, David Sansome + */ +QDBusArgument &operator<<(QDBusArgument &arg, const QImage &image) { + if (image.isNull()) { + arg.beginStructure(); + arg << 0 << 0 << 0 << false << 0 << 0 << QByteArray(); + arg.endStructure(); + return arg; + } + + QImage scaled = image.scaledToHeight(100, Qt::SmoothTransformation); + scaled = scaled.convertToFormat(QImage::Format_ARGB32); + +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + // ABGR -> ARGB + QImage i = scaled.rgbSwapped(); +#else + // ABGR -> GBAR + QImage i(scaled.size(), scaled.format()); + for (int y = 0; y < i.height(); ++y) { + QRgb *p = (QRgb *)scaled.scanLine(y); + QRgb *q = (QRgb *)i.scanLine(y); + QRgb *end = p + scaled.width(); + while (p < end) { + *q = qRgba(qGreen(*p), qBlue(*p), qAlpha(*p), qRed(*p)); + p++; + q++; + } + } +#endif + + arg.beginStructure(); + arg << i.width(); + arg << i.height(); + arg << i.bytesPerLine(); + arg << i.hasAlphaChannel(); + int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3); + arg << i.depth() / channels; + arg << channels; + arg << QByteArray(reinterpret_cast(i.bits()), i.sizeInBytes()); + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, QImage &) { + // This is needed to link but shouldn't be called. + Q_ASSERT(0); + return arg; +} diff --git a/src/notifications/managermac.mm b/src/notifications/managermac.mm new file mode 100644 index 0000000..2b1d362 --- /dev/null +++ b/src/notifications/managermac.mm @@ -0,0 +1,33 @@ +#include "manager.h" + +#include +#include + +@interface NSUserNotification (CFIPrivate) +- (void)set_identityImage:(NSImage *)image; +@end + +NotificationsManager::NotificationsManager(QObject *parent) : QObject(parent) {} + +void NotificationsManager::postNotification(const QString &roomId, const QString &eventId, + const QString &roomName, const QString &senderName, + const QString &text, const QImage &icon) { + Q_UNUSED(roomId); + Q_UNUSED(eventId); + Q_UNUSED(icon); + + NSUserNotification *notif = [[NSUserNotification alloc] init]; + + notif.title = roomName.toNSString(); + notif.subtitle = QString("%1 sent a message").arg(senderName).toNSString(); + notif.informativeText = text.toNSString(); + notif.soundName = NSUserNotificationDefaultSoundName; + + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notif]; + [notif autorelease]; +} + +// unused +void NotificationsManager::actionInvoked(uint, QString) {} + +void NotificationsManager::notificationClosed(uint, uint) {} diff --git a/src/notifications/managerwin.cpp b/src/notifications/managerwin.cpp new file mode 100644 index 0000000..094fe64 --- /dev/null +++ b/src/notifications/managerwin.cpp @@ -0,0 +1,59 @@ +#include "manager.h" +#include "wintoastlib.h" + +using namespace WinToastLib; + +class CustomHandler : public IWinToastHandler { + public: + void toastActivated() const {} + void toastActivated(int) const {} + void toastFailed() const { + std::wcout << L"Error showing current toast" << std::endl; + } + void toastDismissed(WinToastDismissalReason) const {} +}; + +namespace { +bool isInitialized = false; + +void init() { + isInitialized = true; + + WinToast::instance()->setAppName(L"Spectral"); + WinToast::instance()->setAppUserModelId( + WinToast::configureAUMI(L"Spectral", L"Spectral")); + if (!WinToast::instance()->initialize()) + std::wcout << "Your system in not compatible with toast notifications\n"; +} +} // namespace + +NotificationsManager::NotificationsManager(QObject *parent) : QObject(parent) {} + +void NotificationsManager::postNotification( + const QString &room_id, const QString &event_id, const QString &room_name, + const QString &sender, const QString &text, const QImage &icon) { + Q_UNUSED(room_id) + Q_UNUSED(event_id) + Q_UNUSED(icon) + + if (!isInitialized) init(); + + auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02); + if (room_name != sender) + templ.setTextField( + QString("%1 - %2").arg(sender).arg(room_name).toStdWString(), + WinToastTemplate::FirstLine); + else + templ.setTextField(QString("%1").arg(sender).toStdWString(), + WinToastTemplate::FirstLine); + templ.setTextField(QString("%1").arg(text).toStdWString(), + WinToastTemplate::SecondLine); + // TODO: implement room or user avatar + // templ.setImagePath(L"C:/example.png"); + + WinToast::instance()->showToast(templ, new CustomHandler()); +} + +void NotificationsManager::actionInvoked(uint, QString) {} + +void NotificationsManager::notificationClosed(uint, uint) {} diff --git a/src/notifications/wintoastlib.cpp b/src/notifications/wintoastlib.cpp new file mode 100644 index 0000000..2dc8dab --- /dev/null +++ b/src/notifications/wintoastlib.cpp @@ -0,0 +1,1035 @@ +#include "wintoastlib.h" +#include +#include + +#pragma comment(lib,"shlwapi") +#pragma comment(lib,"user32") + +#ifdef NDEBUG + #define DEBUG_MSG(str) do { } while ( false ) + #else + #define DEBUG_MSG(str) do { std::wcout << str << std::endl; } while( false ) +#endif + +// Thanks: https://stackoverflow.com/a/36545162/4297146 + +typedef LONG NTSTATUS, *PNTSTATUS; + +#define STATUS_SUCCESS (0x00000000) + +typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); + +RTL_OSVERSIONINFOW GetRealOSVersion() { + HMODULE hMod = ::GetModuleHandleW(L"ntdll.dll"); + if (hMod) { + RtlGetVersionPtr fxPtr = (RtlGetVersionPtr)::GetProcAddress(hMod, "RtlGetVersion"); + if (fxPtr != nullptr) { + RTL_OSVERSIONINFOW rovi = { 0 }; + rovi.dwOSVersionInfoSize = sizeof(rovi); + if (STATUS_SUCCESS == fxPtr(&rovi)) { + return rovi; + } + } + } + RTL_OSVERSIONINFOW rovi = { 0 }; + return rovi; +} + +// Quickstart: Handling toast activations from Win32 apps in Windows 10 +// https://blogs.msdn.microsoft.com/tiles_and_toasts/2015/10/16/quickstart-handling-toast-activations-from-win32-apps-in-windows-10/ + +using namespace WinToastLib; +namespace DllImporter { + + // Function load a function from library + template + HRESULT loadFunctionFromLibrary(HINSTANCE library, LPCSTR name, Function &func) { + if (!library) { + return E_INVALIDARG; + } + func = reinterpret_cast(GetProcAddress(library, name)); + return (func != nullptr) ? S_OK : E_FAIL; + } + + typedef HRESULT(FAR STDAPICALLTYPE *f_SetCurrentProcessExplicitAppUserModelID)(__in PCWSTR AppID); + typedef HRESULT(FAR STDAPICALLTYPE *f_PropVariantToString)(_In_ REFPROPVARIANT propvar, _Out_writes_(cch) PWSTR psz, _In_ UINT cch); + typedef HRESULT(FAR STDAPICALLTYPE *f_RoGetActivationFactory)(_In_ HSTRING activatableClassId, _In_ REFIID iid, _COM_Outptr_ void ** factory); + 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); + typedef PCWSTR(FAR STDAPICALLTYPE *f_WindowsGetStringRawBuffer)(_In_ HSTRING string, _Out_ UINT32 *length); + typedef HRESULT(FAR STDAPICALLTYPE *f_WindowsDeleteString)(_In_opt_ HSTRING string); + + static f_SetCurrentProcessExplicitAppUserModelID SetCurrentProcessExplicitAppUserModelID; + static f_PropVariantToString PropVariantToString; + static f_RoGetActivationFactory RoGetActivationFactory; + static f_WindowsCreateStringReference WindowsCreateStringReference; + static f_WindowsGetStringRawBuffer WindowsGetStringRawBuffer; + static f_WindowsDeleteString WindowsDeleteString; + + + 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()); + } + + inline HRESULT initialize() { + HINSTANCE LibShell32 = LoadLibraryW(L"SHELL32.DLL"); + HRESULT hr = loadFunctionFromLibrary(LibShell32, "SetCurrentProcessExplicitAppUserModelID", SetCurrentProcessExplicitAppUserModelID); + if (SUCCEEDED(hr)) { + HINSTANCE LibPropSys = LoadLibraryW(L"PROPSYS.DLL"); + hr = loadFunctionFromLibrary(LibPropSys, "PropVariantToString", PropVariantToString); + if (SUCCEEDED(hr)) { + HINSTANCE LibComBase = LoadLibraryW(L"COMBASE.DLL"); + const bool succeded = SUCCEEDED(loadFunctionFromLibrary(LibComBase, "RoGetActivationFactory", RoGetActivationFactory)) + && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsCreateStringReference", WindowsCreateStringReference)) + && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsGetStringRawBuffer", WindowsGetStringRawBuffer)) + && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsDeleteString", WindowsDeleteString)); + return succeded ? S_OK : E_FAIL; + } + } + return hr; + } +} + +class WinToastStringWrapper { +public: + WinToastStringWrapper(_In_reads_(length) PCWSTR stringRef, _In_ UINT32 length) throw() { + HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef, length, &_header, &_hstring); + if (!SUCCEEDED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + } + WinToastStringWrapper(_In_ const std::wstring &stringRef) throw() { + HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef.c_str(), static_cast(stringRef.length()), &_header, &_hstring); + if (FAILED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + } + ~WinToastStringWrapper() { + DllImporter::WindowsDeleteString(_hstring); + } + inline HSTRING Get() const throw() { return _hstring; } +private: + HSTRING _hstring; + HSTRING_HEADER _header; + +}; + +class MyDateTime : public IReference +{ +protected: + DateTime _dateTime; + +public: + static INT64 Now() { + FILETIME now; + GetSystemTimeAsFileTime(&now); + return ((((INT64)now.dwHighDateTime) << 32) | now.dwLowDateTime); + } + + MyDateTime(DateTime dateTime) : _dateTime(dateTime) {} + + MyDateTime(INT64 millisecondsFromNow) { + _dateTime.UniversalTime = Now() + millisecondsFromNow * 10000; + } + + operator INT64() { + return _dateTime.UniversalTime; + } + + HRESULT STDMETHODCALLTYPE get_Value(DateTime *dateTime) { + *dateTime = _dateTime; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryInterface(const IID& riid, void** ppvObject) { + if (!ppvObject) { + return E_POINTER; + } + if (riid == __uuidof(IUnknown) || riid == __uuidof(IReference)) { + *ppvObject = static_cast(static_cast*>(this)); + return S_OK; + } + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE Release() { + return 1; + } + + ULONG STDMETHODCALLTYPE AddRef() { + return 2; + } + + HRESULT STDMETHODCALLTYPE GetIids(ULONG*, IID**) { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE GetRuntimeClassName(HSTRING*) { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE GetTrustLevel(TrustLevel*) { + return E_NOTIMPL; + } +}; + +namespace Util { + inline HRESULT defaultExecutablePath(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + DWORD written = GetModuleFileNameExW(GetCurrentProcess(), nullptr, path, nSize); + DEBUG_MSG("Default executable path: " << path); + return (written > 0) ? S_OK : E_FAIL; + } + + + inline HRESULT defaultShellLinksDirectory(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + DWORD written = GetEnvironmentVariableW(L"APPDATA", path, nSize); + HRESULT hr = written > 0 ? S_OK : E_INVALIDARG; + if (SUCCEEDED(hr)) { + errno_t result = wcscat_s(path, nSize, DEFAULT_SHELL_LINKS_PATH); + hr = (result == 0) ? S_OK : E_INVALIDARG; + DEBUG_MSG("Default shell link path: " << path); + } + return hr; + } + + inline HRESULT defaultShellLinkPath(const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + HRESULT hr = defaultShellLinksDirectory(path, nSize); + if (SUCCEEDED(hr)) { + const std::wstring appLink(appname + DEFAULT_LINK_FORMAT); + errno_t result = wcscat_s(path, nSize, appLink.c_str()); + hr = (result == 0) ? S_OK : E_INVALIDARG; + DEBUG_MSG("Default shell link file path: " << path); + } + return hr; + } + + + inline PCWSTR AsString(ComPtr &xmlDocument) { + HSTRING xml; + ComPtr ser; + HRESULT hr = xmlDocument.As(&ser); + hr = ser->GetXml(&xml); + if (SUCCEEDED(hr)) + return DllImporter::WindowsGetStringRawBuffer(xml, NULL); + return NULL; + } + + inline PCWSTR AsString(HSTRING hstring) { + return DllImporter::WindowsGetStringRawBuffer(hstring, NULL); + } + + inline HRESULT setNodeStringValue(const std::wstring& string, IXmlNode *node, IXmlDocument *xml) { + ComPtr textNode; + HRESULT hr = xml->CreateTextNode( WinToastStringWrapper(string).Get(), &textNode); + if (SUCCEEDED(hr)) { + ComPtr stringNode; + hr = textNode.As(&stringNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = node->AppendChild(stringNode.Get(), &appendedChild); + } + } + return hr; + } + + inline HRESULT setEventHandlers(_In_ IToastNotification* notification, _In_ std::shared_ptr eventHandler, _In_ INT64 expirationTime) { + EventRegistrationToken activatedToken, dismissedToken, failedToken; + HRESULT hr = notification->add_Activated( + Callback < Implements < RuntimeClassFlags, + ITypedEventHandler> >( + [eventHandler](IToastNotification*, IInspectable* inspectable) + { + IToastActivatedEventArgs *activatedEventArgs; + HRESULT hr = inspectable->QueryInterface(&activatedEventArgs); + if (SUCCEEDED(hr)) { + HSTRING argumentsHandle; + hr = activatedEventArgs->get_Arguments(&argumentsHandle); + if (SUCCEEDED(hr)) { + PCWSTR arguments = Util::AsString(argumentsHandle); + if (arguments && *arguments) { + eventHandler->toastActivated((int)wcstol(arguments, NULL, 10)); + return S_OK; + } + } + } + eventHandler->toastActivated(); + return S_OK; + }).Get(), &activatedToken); + + if (SUCCEEDED(hr)) { + hr = notification->add_Dismissed(Callback < Implements < RuntimeClassFlags, + ITypedEventHandler> >( + [eventHandler, expirationTime](IToastNotification*, IToastDismissedEventArgs* e) + { + ToastDismissalReason reason; + if (SUCCEEDED(e->get_Reason(&reason))) + { + if (reason == ToastDismissalReason_UserCanceled && expirationTime && MyDateTime::Now() >= expirationTime) + reason = ToastDismissalReason_TimedOut; + eventHandler->toastDismissed(static_cast(reason)); + } + return S_OK; + }).Get(), &dismissedToken); + if (SUCCEEDED(hr)) { + hr = notification->add_Failed(Callback < Implements < RuntimeClassFlags, + ITypedEventHandler> >( + [eventHandler](IToastNotification*, IToastFailedEventArgs*) + { + eventHandler->toastFailed(); + return S_OK; + }).Get(), &failedToken); + } + } + return hr; + } + + inline HRESULT addAttribute(_In_ IXmlDocument *xml, const std::wstring &name, IXmlNamedNodeMap *attributeMap) { + ComPtr srcAttribute; + HRESULT hr = xml->CreateAttribute(WinToastStringWrapper(name).Get(), &srcAttribute); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = srcAttribute.As(&node); + if (SUCCEEDED(hr)) { + ComPtr pNode; + hr = attributeMap->SetNamedItem(node.Get(), &pNode); + } + } + return hr; + } + + inline HRESULT createElement(_In_ IXmlDocument *xml, _In_ const std::wstring& root_node, _In_ const std::wstring& element_name, _In_ const std::vector& attribute_names) { + ComPtr rootList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(root_node).Get(), &rootList); + if (SUCCEEDED(hr)) { + ComPtr root; + hr = rootList->Item(0, &root); + if (SUCCEEDED(hr)) { + ComPtr audioElement; + hr = xml->CreateElement(WinToastStringWrapper(element_name).Get(), &audioElement); + if (SUCCEEDED(hr)) { + ComPtr audioNodeTmp; + hr = audioElement.As(&audioNodeTmp); + if (SUCCEEDED(hr)) { + ComPtr audioNode; + hr = root->AppendChild(audioNodeTmp.Get(), &audioNode); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = audioNode->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + for (auto it : attribute_names) { + hr = addAttribute(xml, it, attributes.Get()); + } + } + } + } + } + } + } + return hr; + } +} + +WinToast* WinToast::instance() { + static WinToast instance; + return &instance; +} + +WinToast::WinToast() : + _isInitialized(false), + _hasCoInitialized(false) +{ + if (!isCompatible()) { + DEBUG_MSG(L"Warning: Your system is not compatible with this library "); + } +} + +WinToast::~WinToast() { + if (_hasCoInitialized) { + CoUninitialize(); + } +} + +void WinToast::setAppName(_In_ const std::wstring& appName) { + _appName = appName; +} + + +void WinToast::setAppUserModelId(_In_ const std::wstring& aumi) { + _aumi = aumi; + DEBUG_MSG(L"Default App User Model Id: " << _aumi.c_str()); +} + +bool WinToast::isCompatible() { + DllImporter::initialize(); + return !((DllImporter::SetCurrentProcessExplicitAppUserModelID == nullptr) + || (DllImporter::PropVariantToString == nullptr) + || (DllImporter::RoGetActivationFactory == nullptr) + || (DllImporter::WindowsCreateStringReference == nullptr) + || (DllImporter::WindowsDeleteString == nullptr)); +} + +bool WinToastLib::WinToast::isSupportingModernFeatures() { + RTL_OSVERSIONINFOW tmp = GetRealOSVersion(); + return tmp.dwMajorVersion > 6; + +} +std::wstring WinToast::configureAUMI(_In_ const std::wstring &companyName, + _In_ const std::wstring &productName, + _In_ const std::wstring &subProduct, + _In_ const std::wstring &versionInformation) +{ + std::wstring aumi = companyName; + aumi += L"." + productName; + if (subProduct.length() > 0) { + aumi += L"." + subProduct; + if (versionInformation.length() > 0) { + aumi += L"." + versionInformation; + } + } + + if (aumi.length() > SCHAR_MAX) { + DEBUG_MSG("Error: max size allowed for AUMI: 128 characters."); + } + return aumi; +} + + +enum WinToast::ShortcutResult WinToast::createShortcut() { + if (_aumi.empty() || _appName.empty()) { + DEBUG_MSG(L"Error: App User Model Id or Appname is empty!"); + return SHORTCUT_MISSING_PARAMETERS; + } + + if (!isCompatible()) { + DEBUG_MSG(L"Your OS is not compatible with this library! =("); + return SHORTCUT_INCOMPATIBLE_OS; + } + + if (!_hasCoInitialized) { + HRESULT initHr = CoInitializeEx(NULL, COINIT::COINIT_MULTITHREADED); + if (initHr != RPC_E_CHANGED_MODE) { + if (FAILED(initHr) && initHr != S_FALSE) { + DEBUG_MSG(L"Error on COM library initialization!"); + return SHORTCUT_COM_INIT_FAILURE; + } + else { + _hasCoInitialized = true; + } + } + } + + bool wasChanged; + HRESULT hr = validateShellLinkHelper(wasChanged); + if (SUCCEEDED(hr)) + return wasChanged ? SHORTCUT_WAS_CHANGED : SHORTCUT_UNCHANGED; + + hr = createShellLinkHelper(); + return SUCCEEDED(hr) ? SHORTCUT_WAS_CREATED : SHORTCUT_CREATE_FAILED; +} + +bool WinToast::initialize(_Out_ WinToastError* error) { + _isInitialized = false; + setError(error, WinToastError::NoError); + + if (!isCompatible()) { + setError(error, WinToastError::SystemNotSupported); + DEBUG_MSG(L"Error: system not supported."); + return false; + } + + + if (_aumi.empty() || _appName.empty()) { + setError(error, WinToastError::InvalidParameters); + DEBUG_MSG(L"Error while initializing, did you set up a valid AUMI and App name?"); + return false; + } + + if (createShortcut() < 0) { + setError(error, WinToastError::ShellLinkNotCreated); + DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =("); + return false; + } + + if (FAILED(DllImporter::SetCurrentProcessExplicitAppUserModelID(_aumi.c_str()))) { + setError(error, WinToastError::InvalidAppUserModelID); + DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =("); + return false; + } + + _isInitialized = true; + return _isInitialized; +} + +bool WinToast::isInitialized() const { + return _isInitialized; +} + +const std::wstring& WinToast::appName() const { + return _appName; +} + +const std::wstring& WinToast::appUserModelId() const { + return _aumi; +} + + +HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) { + WCHAR path[MAX_PATH] = { L'\0' }; + Util::defaultShellLinkPath(_appName, path); + // Check if the file exist + DWORD attr = GetFileAttributesW(path); + if (attr >= 0xFFFFFFF) { + DEBUG_MSG("Error, shell link not found. Try to create a new one in: " << path); + return E_FAIL; + } + + // Let's load the file as shell link to validate. + // - Create a shell link + // - Create a persistant file + // - Load the path as data for the persistant file + // - Read the property AUMI and validate with the current + // - Review if AUMI is equal. + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (SUCCEEDED(hr)) { + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (SUCCEEDED(hr)) { + hr = persistFile->Load(path, STGM_READWRITE); + if (SUCCEEDED(hr)) { + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (SUCCEEDED(hr)) { + PROPVARIANT appIdPropVar; + hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &appIdPropVar); + if (SUCCEEDED(hr)) { + WCHAR AUMI[MAX_PATH]; + hr = DllImporter::PropVariantToString(appIdPropVar, AUMI, MAX_PATH); + wasChanged = false; + if (FAILED(hr) || _aumi != AUMI) { + // AUMI Changed for the same app, let's update the current value! =) + wasChanged = true; + PropVariantClear(&appIdPropVar); + hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->Commit(); + if (SUCCEEDED(hr) && SUCCEEDED(persistFile->IsDirty())) { + hr = persistFile->Save(path, TRUE); + } + } + } + } + PropVariantClear(&appIdPropVar); + } + } + } + } + } + return hr; +} + + + +HRESULT WinToast::createShellLinkHelper() { + WCHAR exePath[MAX_PATH]{L'\0'}; + WCHAR slPath[MAX_PATH]{L'\0'}; + Util::defaultShellLinkPath(_appName, slPath); + Util::defaultExecutablePath(exePath); + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (SUCCEEDED(hr)) { + hr = shellLink->SetPath(exePath); + if (SUCCEEDED(hr)) { + hr = shellLink->SetArguments(L""); + if (SUCCEEDED(hr)) { + hr = shellLink->SetWorkingDirectory(exePath); + if (SUCCEEDED(hr)) { + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (SUCCEEDED(hr)) { + PROPVARIANT appIdPropVar; + hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->Commit(); + if (SUCCEEDED(hr)) { + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (SUCCEEDED(hr)) { + hr = persistFile->Save(slPath, TRUE); + } + } + } + PropVariantClear(&appIdPropVar); + } + } + } + } + } + } + return hr; +} + +INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error) { + setError(error, WinToastError::NoError); + INT64 id = -1; + if (!isInitialized()) { + setError(error, WinToastError::NotInitialized); + DEBUG_MSG("Error when launching the toast. WinToast is not initialized."); + return id; + } + if (!handler) { + setError(error, WinToastError::InvalidHandler); + DEBUG_MSG("Error when launching the toast. Handler cannot be null."); + return id; + } + + ComPtr notificationManager; + HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + if (SUCCEEDED(hr)) { + ComPtr notifier; + hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); + if (SUCCEEDED(hr)) { + ComPtr notificationFactory; + hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), ¬ificationFactory); + if (SUCCEEDED(hr)) { + ComPtr xmlDocument; + HRESULT hr = notificationManager->GetTemplateContent(ToastTemplateType(toast.type()), &xmlDocument); + if (SUCCEEDED(hr)) { + const int fieldsCount = toast.textFieldsCount(); + for (int i = 0; i < fieldsCount && SUCCEEDED(hr); i++) { + hr = setTextFieldHelper(xmlDocument.Get(), toast.textField(WinToastTemplate::TextField(i)), i); + } + + // Modern feature are supported Windows > Windows 10 + if (SUCCEEDED(hr) && isSupportingModernFeatures()) { + + // Note that we do this *after* using toast.textFieldsCount() to + // iterate/fill the template's text fields, since we're adding yet another text field. + if (SUCCEEDED(hr) + && !toast.attributionText().empty()) { + hr = setAttributionTextFieldHelper(xmlDocument.Get(), toast.attributionText()); + } + + const int actionsCount = toast.actionsCount(); + WCHAR buf[12]; + for (int i = 0; i < actionsCount && SUCCEEDED(hr); i++) { + _snwprintf_s(buf, sizeof(buf) / sizeof(*buf), _TRUNCATE, L"%d", i); + hr = addActionHelper(xmlDocument.Get(), toast.actionLabel(i), buf); + } + + if (SUCCEEDED(hr)) { + hr = (toast.audioPath().empty() && toast.audioOption() == WinToastTemplate::AudioOption::Default) + ? hr : setAudioFieldHelper(xmlDocument.Get(), toast.audioPath(), toast.audioOption()); + } + + if (SUCCEEDED(hr) && toast.duration() != WinToastTemplate::Duration::System) { + hr = addDurationHelper(xmlDocument.Get(), + (toast.duration() == WinToastTemplate::Duration::Short) ? L"short" : L"long"); + } + + } else { + DEBUG_MSG("Modern features (Actions/Sounds/Attributes) not supported in this os version"); + } + + if (SUCCEEDED(hr)) { + hr = toast.hasImage() ? setImageFieldHelper(xmlDocument.Get(), toast.imagePath()) : hr; + if (SUCCEEDED(hr)) { + ComPtr notification; + hr = notificationFactory->CreateToastNotification(xmlDocument.Get(), ¬ification); + if (SUCCEEDED(hr)) { + INT64 expiration = 0, relativeExpiration = toast.expiration(); + if (relativeExpiration > 0) { + MyDateTime expirationDateTime(relativeExpiration); + expiration = expirationDateTime; + hr = notification->put_ExpirationTime(&expirationDateTime); + } + + if (SUCCEEDED(hr)) { + hr = Util::setEventHandlers(notification.Get(), std::shared_ptr(handler), expiration); + if (FAILED(hr)) { + setError(error, WinToastError::InvalidHandler); + } + } + + if (SUCCEEDED(hr)) { + GUID guid; + hr = CoCreateGuid(&guid); + if (SUCCEEDED(hr)) { + id = guid.Data1; + _buffer[id] = notification; + DEBUG_MSG("xml: " << Util::AsString(xmlDocument)); + hr = notifier->Show(notification.Get()); + if (FAILED(hr)) { + setError(error, WinToastError::NotDisplayed); + } + } + } + } + } + } + } + } + } + } + return FAILED(hr) ? -1 : id; +} + +ComPtr WinToast::notifier(_In_ bool* succeded) const { + ComPtr notificationManager; + ComPtr notifier; + HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + if (SUCCEEDED(hr)) { + hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); + } + *succeded = SUCCEEDED(hr); + return notifier; +} + +bool WinToast::hideToast(_In_ INT64 id) { + if (!isInitialized()) { + DEBUG_MSG("Error when hiding the toast. WinToast is not initialized."); + return false; + } + const bool find = _buffer.find(id) != _buffer.end(); + if (find) { + bool succeded = false; + ComPtr notify = notifier(&succeded); + if (succeded) { + notify->Hide(_buffer[id].Get()); + } + _buffer.erase(id); + } + return find; +} + +void WinToast::clear() { + bool succeded = false; + ComPtr notify = notifier(&succeded); + if (succeded) { + auto end = _buffer.end(); + for (auto it = _buffer.begin(); it != end; ++it) { + notify->Hide(it->second.Get()); + } + } + _buffer.clear(); +} + +// +// Available as of Windows 10 Anniversary Update +// Ref: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts +// +// NOTE: This will add a new text field, so be aware when iterating over +// the toast's text fields or getting a count of them. +// +HRESULT WinToast::setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text) { + Util::createElement(xml, L"binding", L"text", { L"placement" }); + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 nodeListLength; + hr = nodeList->get_Length(&nodeListLength); + if (SUCCEEDED(hr)) { + for (UINT32 i = 0; i < nodeListLength; i++) { + ComPtr textNode; + hr = nodeList->Item(i, &textNode); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = textNode->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + if (SUCCEEDED(hr)) { + hr = attributes->GetNamedItem(WinToastStringWrapper(L"placement").Get(), &editedNode); + if (FAILED(hr) || !editedNode) { + continue; + } + hr = Util::setNodeStringValue(L"attribution", editedNode.Get(), xml); + if (SUCCEEDED(hr)) { + return setTextFieldHelper(xml, text, i); + } + } + } + } + } + } + } + return hr; +} + +HRESULT WinToast::addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), + WinToastStringWrapper(duration).Get()); + } + } + } + } + return hr; +} + +HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ int pos) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(pos, &node); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(text, node.Get(), xml); + } + } + return hr; +} + + +HRESULT WinToast::setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path) { + wchar_t imagePath[MAX_PATH] = L"file:///"; + HRESULT hr = StringCchCatW(imagePath, MAX_PATH, path.c_str()); + if (SUCCEEDED(hr)) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"image").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(0, &node); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = node->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode); + if (SUCCEEDED(hr)) { + Util::setNodeStringValue(imagePath, editedNode.Get(), xml); + } + } + } + } + } + return hr; +} + +HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option) { + std::vector attrs; + if (!path.empty()) attrs.push_back(L"src"); + if (option == WinToastTemplate::AudioOption::Loop) attrs.push_back(L"loop"); + if (option == WinToastTemplate::AudioOption::Silent) attrs.push_back(L"silent"); + Util::createElement(xml, L"toast", L"audio", attrs); + + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"audio").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(0, &node); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = node->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + if (!path.empty()) { + if (SUCCEEDED(hr)) { + hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(path, editedNode.Get(), xml); + } + } + } + + if (SUCCEEDED(hr)) { + switch (option) { + case WinToastTemplate::AudioOption::Loop: + hr = attributes->GetNamedItem(WinToastStringWrapper(L"loop").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); + } + break; + case WinToastTemplate::AudioOption::Silent: + hr = attributes->GetNamedItem(WinToastStringWrapper(L"silent").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); + } + default: + break; + } + } + } + } + } + return hr; +} + +HRESULT WinToast::addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& content, _In_ const std::wstring& arguments) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"actions").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr actionsNode; + if (length > 0) { + hr = nodeList->Item(0, &actionsNode); + } else { + hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); + if (SUCCEEDED(hr)) { + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) + hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), WinToastStringWrapper(L"ToastGeneric").Get()); + if (SUCCEEDED(hr)) + hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), WinToastStringWrapper(L"long").Get()); + if (SUCCEEDED(hr)) { + ComPtr actionsElement; + hr = xml->CreateElement(WinToastStringWrapper(L"actions").Get(), &actionsElement); + if (SUCCEEDED(hr)) { + hr = actionsElement.As(&actionsNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = toastNode->AppendChild(actionsNode.Get(), &appendedChild); + } + } + } + } + } + } + } + if (SUCCEEDED(hr)) { + ComPtr actionElement; + hr = xml->CreateElement(WinToastStringWrapper(L"action").Get(), &actionElement); + if (SUCCEEDED(hr)) + hr = actionElement->SetAttribute(WinToastStringWrapper(L"content").Get(), WinToastStringWrapper(content).Get()); + if (SUCCEEDED(hr)) + hr = actionElement->SetAttribute(WinToastStringWrapper(L"arguments").Get(), WinToastStringWrapper(arguments).Get()); + if (SUCCEEDED(hr)) { + ComPtr actionNode; + hr = actionElement.As(&actionNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild); + } + } + } + } + } + return hr; +} + +void WinToast::setError(_Out_ WinToastError* error, _In_ WinToastError value) { + if (error) { + *error = value; + } +} + +WinToastTemplate::WinToastTemplate(_In_ WinToastTemplateType type) : _type(type) { + static const std::size_t TextFieldsCount[] = { 1, 2, 2, 3, 1, 2, 2, 3}; + _textFields = std::vector(TextFieldsCount[type], L""); +} + +WinToastTemplate::~WinToastTemplate() { + _textFields.clear(); +} + +void WinToastTemplate::setTextField(_In_ const std::wstring& txt, _In_ WinToastTemplate::TextField pos) { + _textFields[pos] = txt; +} + +void WinToastTemplate::setImagePath(_In_ const std::wstring& imgPath) { + _imagePath = imgPath; +} + +void WinToastTemplate::setAudioPath(_In_ const std::wstring& audioPath) { + _audioPath = audioPath; +} + +void WinToastTemplate::setAudioOption(_In_ WinToastTemplate::AudioOption audioOption) { + _audioOption = audioOption; +} + +void WinToastTemplate::setDuration(_In_ Duration duration) { + _duration = duration; +} + +void WinToastTemplate::setExpiration(_In_ INT64 millisecondsFromNow) { + _expiration = millisecondsFromNow; +} + +void WinToastTemplate::setAttributionText(_In_ const std::wstring& attributionText) { + _attributionText = attributionText; +} + +void WinToastTemplate::addAction(_In_ const std::wstring & label) +{ + _actions.push_back(label); +} + +std::size_t WinToastTemplate::textFieldsCount() const { + return _textFields.size(); +} + +std::size_t WinToastTemplate::actionsCount() const { + return _actions.size(); +} + +bool WinToastTemplate::hasImage() const { + return _type < WinToastTemplateType::Text01; +} + +const std::vector& WinToastTemplate::textFields() const { + return _textFields; +} + +const std::wstring& WinToastTemplate::textField(_In_ TextField pos) const { + return _textFields[pos]; +} + +const std::wstring& WinToastTemplate::actionLabel(_In_ int pos) const { + return _actions[pos]; +} + +const std::wstring& WinToastTemplate::imagePath() const { + return _imagePath; +} + +const std::wstring& WinToastTemplate::audioPath() const { + return _audioPath; +} + +const std::wstring& WinToastTemplate::attributionText() const { + return _attributionText; +} + +INT64 WinToastTemplate::expiration() const { + return _expiration; +} + +WinToastTemplate::WinToastTemplateType WinToastTemplate::type() const { + return _type; +} + +WinToastTemplate::AudioOption WinToastTemplate::audioOption() const { + return _audioOption; +} + +WinToastTemplate::Duration WinToastTemplate::duration() const { + return _duration; +} diff --git a/src/notifications/wintoastlib.h b/src/notifications/wintoastlib.h new file mode 100644 index 0000000..e991d66 --- /dev/null +++ b/src/notifications/wintoastlib.h @@ -0,0 +1,164 @@ +#ifndef WINTOASTLIB_H +#define WINTOASTLIB_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace Microsoft::WRL; +using namespace ABI::Windows::Data::Xml::Dom; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::UI::Notifications; +using namespace Windows::Foundation; + +#define DEFAULT_SHELL_LINKS_PATH L"\\Microsoft\\Windows\\Start Menu\\Programs\\" +#define DEFAULT_LINK_FORMAT L".lnk" +namespace WinToastLib { + + class IWinToastHandler { + public: + enum WinToastDismissalReason { + UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled, + ApplicationHidden = ToastDismissalReason::ToastDismissalReason_ApplicationHidden, + TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut + }; + virtual ~IWinToastHandler() {} + virtual void toastActivated() const = 0; + virtual void toastActivated(int actionIndex) const = 0; + virtual void toastDismissed(WinToastDismissalReason state) const = 0; + virtual void toastFailed() const = 0; + }; + + class WinToastTemplate { + public: + enum Duration { System, Short, Long }; + enum AudioOption { Default = 0, Silent = 1, Loop = 2 }; + enum TextField { FirstLine = 0, SecondLine, ThirdLine }; + enum WinToastTemplateType { + ImageAndText01 = ToastTemplateType::ToastTemplateType_ToastImageAndText01, + ImageAndText02 = ToastTemplateType::ToastTemplateType_ToastImageAndText02, + ImageAndText03 = ToastTemplateType::ToastTemplateType_ToastImageAndText03, + ImageAndText04 = ToastTemplateType::ToastTemplateType_ToastImageAndText04, + Text01 = ToastTemplateType::ToastTemplateType_ToastText01, + Text02 = ToastTemplateType::ToastTemplateType_ToastText02, + Text03 = ToastTemplateType::ToastTemplateType_ToastText03, + Text04 = ToastTemplateType::ToastTemplateType_ToastText04, + WinToastTemplateTypeCount + }; + + WinToastTemplate(_In_ WinToastTemplateType type = WinToastTemplateType::ImageAndText02); + ~WinToastTemplate(); + + void setTextField(_In_ const std::wstring& txt, _In_ TextField pos); + void setImagePath(_In_ const std::wstring& imgPath); + void setAudioPath(_In_ const std::wstring& audioPath); + void setAttributionText(_In_ const std::wstring & attributionText); + void addAction(_In_ const std::wstring& label); + void setAudioOption(_In_ WinToastTemplate::AudioOption audioOption); + void setDuration(_In_ Duration duration); + void setExpiration(_In_ INT64 millisecondsFromNow); + std::size_t textFieldsCount() const; + std::size_t actionsCount() const; + bool hasImage() const; + const std::vector& textFields() const; + const std::wstring& textField(_In_ TextField pos) const; + const std::wstring& actionLabel(_In_ int pos) const; + const std::wstring& imagePath() const; + const std::wstring& audioPath() const; + const std::wstring& attributionText() const; + INT64 expiration() const; + WinToastTemplateType type() const; + WinToastTemplate::AudioOption audioOption() const; + Duration duration() const; + private: + std::vector _textFields; + std::vector _actions; + std::wstring _imagePath = L""; + std::wstring _audioPath = L""; + std::wstring _attributionText = L""; + INT64 _expiration = 0; + AudioOption _audioOption = WinToastTemplate::AudioOption::Default; + WinToastTemplateType _type = WinToastTemplateType::Text01; + Duration _duration = Duration::System; + }; + + class WinToast { + public: + enum WinToastError { + NoError = 0, + NotInitialized, + SystemNotSupported, + ShellLinkNotCreated, + InvalidAppUserModelID, + InvalidParameters, + InvalidHandler, + NotDisplayed, + UnknownError + }; + + enum ShortcutResult { + SHORTCUT_UNCHANGED = 0, + SHORTCUT_WAS_CHANGED = 1, + SHORTCUT_WAS_CREATED = 2, + + SHORTCUT_MISSING_PARAMETERS = -1, + SHORTCUT_INCOMPATIBLE_OS = -2, + SHORTCUT_COM_INIT_FAILURE = -3, + SHORTCUT_CREATE_FAILED = -4 + }; + + WinToast(void); + virtual ~WinToast(); + static WinToast* instance(); + static bool isCompatible(); + static bool isSupportingModernFeatures(); + static std::wstring configureAUMI(_In_ const std::wstring& companyName, + _In_ const std::wstring& productName, + _In_ const std::wstring& subProduct = std::wstring(), + _In_ const std::wstring& versionInformation = std::wstring() + ); + virtual bool initialize(_Out_ WinToastError* error = nullptr); + virtual bool isInitialized() const; + virtual bool hideToast(_In_ INT64 id); + virtual INT64 showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error = nullptr); + virtual void clear(); + virtual enum ShortcutResult createShortcut(); + + const std::wstring& appName() const; + const std::wstring& appUserModelId() const; + void setAppUserModelId(_In_ const std::wstring& appName); + void setAppName(_In_ const std::wstring& appName); + + protected: + bool _isInitialized; + bool _hasCoInitialized; + std::wstring _appName; + std::wstring _aumi; + std::map> _buffer; + + HRESULT validateShellLinkHelper(_Out_ bool& wasChanged); + HRESULT createShellLinkHelper(); + HRESULT setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path); + HRESULT setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option = WinToastTemplate::AudioOption::Default); + HRESULT setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ int pos); + HRESULT setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text); + HRESULT addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& action, _In_ const std::wstring& arguments); + HRESULT addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration); + ComPtr notifier(_In_ bool* succeded) const; + void setError(_Out_ WinToastError* error, _In_ WinToastError value); + }; +} +#endif // WINTOASTLIB_H diff --git a/src/roomlistmodel.cpp b/src/roomlistmodel.cpp index 5d7d240..2d20532 100644 --- a/src/roomlistmodel.cpp +++ b/src/roomlistmodel.cpp @@ -80,10 +80,10 @@ void RoomListModel::connectRoomSignals(SpectralRoom* room) { if (event->isStateEvent()) return; User* sender = room->user(event->senderId()); if (sender == room->localUser()) return; - emit newMessage(room->displayName(), - sender->displayname() + ": " + - event->contentJson().value("body").toString(), - QPixmap::fromImage(room->avatar(64))); + emit newMessage(room->id(), event->id(), room->displayName(), + sender->displayname(), + event->contentJson().value("body").toString(), + room->avatar(64)); }); } diff --git a/src/roomlistmodel.h b/src/roomlistmodel.h index 418a8fb..9da486d 100644 --- a/src/roomlistmodel.h +++ b/src/roomlistmodel.h @@ -74,8 +74,9 @@ class RoomListModel : public QAbstractListModel { signals: void connectionChanged(); void roomAdded(SpectralRoom* room); - void newMessage(const QString& roomName, const QString& content, - const QIcon& icon); + void newMessage(const QString& roomId, const QString& eventId, + const QString& roomName, const QString& senderName, + const QString& text, const QImage& icon); }; #endif // ROOMLISTMODEL_H