Notification improvements.

square-messages
Black Hat 2018-10-19 22:02:12 +08:00
parent a0fcd00a6f
commit bb069197d6
16 changed files with 1559 additions and 48 deletions

View File

@ -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)
}

View File

@ -15,6 +15,7 @@ Page {
property alias filter: roomListForm.filter
property alias roomListModel: roomListModel
property alias enteredRoom: roomListForm.enteredRoom
id: page

View File

@ -36,6 +36,12 @@ Rectangle {
}
onClicked: currentRoom.chooseAndUploadFile()
BusyIndicator {
anchors.fill: parent
running: false
}
}
ScrollView {

View File

@ -38,6 +38,10 @@ ApplicationWindow {
window.requestActivate()
}
onHideWindow: window.hide()
onNotificationClicked: {
roomPage.enteredRoom = currentConnection.room(roomId)
showWindow()
}
onErrorOccured: {
errorDialog.error = error
errorDialog.detail = detail

View File

@ -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
}

View File

@ -27,18 +27,20 @@
#include <QtNetwork/QAuthenticator>
#include <QtNetwork/QNetworkReply>
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(&notificationsManager, &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<SpectralRoom>();
Connection::setUserType<SpectralUser>();
@ -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);
}

View File

@ -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<Connection*> 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);
};

View File

@ -49,8 +49,9 @@ int main(int argc, char *argv[]) {
qmlRegisterUncreatableType<RoomType>("Spectral", 0, 1, "RoomType", "ENUM");
qRegisterMetaType<User *>("User*");
qRegisterMetaType<Room *>("Room*");
qRegisterMetaType<MessageEventType>("MessageEventType");
qRegisterMetaType<SpectralRoom *>("SpectralRoom");
qRegisterMetaType<SpectralRoom *>("SpectralRoom*");
QQmlApplicationEngine engine;

View File

@ -0,0 +1,49 @@
#pragma once
#include <QImage>
#include <QObject>
#include <QString>
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
#include <QtDBus/QDBusArgument>
#include <QtDBus/QDBusInterface>
#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<uint, roomEventId> 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

View File

@ -0,0 +1,133 @@
#include "manager.h"
#include <QDebug>
#include <QImage>
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusMessage>
#include <QtDBus/QDBusMetaType>
NotificationsManager::NotificationsManager(QObject *parent)
: QObject(parent),
dbus("org.freedesktop.Notifications", "/org/freedesktop/Notifications",
"org.freedesktop.Notifications", QDBusConnection::sessionBus(),
this) {
qDBusRegisterMetaType<QImage>();
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 <rohieb@rohieb.name>
* 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<QVariant> 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 <me@davidsansome.com>
*/
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<const char *>(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;
}

View File

@ -0,0 +1,33 @@
#include "manager.h"
#include <Foundation/Foundation.h>
#include <QtMac>
@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) {}

View File

@ -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) {}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,164 @@
#ifndef WINTOASTLIB_H
#define WINTOASTLIB_H
#include <Windows.h>
#include <sdkddkver.h>
#include <WinUser.h>
#include <ShObjIdl.h>
#include <wrl/implements.h>
#include <wrl/event.h>
#include <windows.ui.notifications.h>
#include <strsafe.h>
#include <Psapi.h>
#include <ShlObj.h>
#include <roapi.h>
#include <propvarutil.h>
#include <functiondiscoverykeys.h>
#include <iostream>
#include <winstring.h>
#include <string.h>
#include <vector>
#include <map>
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<std::wstring>& 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<std::wstring> _textFields;
std::vector<std::wstring> _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<INT64, ComPtr<IToastNotification>> _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<IToastNotifier> notifier(_In_ bool* succeded) const;
void setError(_Out_ WinToastError* error, _In_ WinToastError value);
};
}
#endif // WINTOASTLIB_H

View File

@ -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));
});
}

View File

@ -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