diff --git a/CMakeLists.txt b/CMakeLists.txt index 7aa75a3e..48d739d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,7 @@ include(LMDB) # Discover Qt dependencies. # find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia REQUIRED) +find_package(Qt5DBus) if (APPLE) find_package(Qt5MacExtras REQUIRED) @@ -304,6 +305,8 @@ qt5_wrap_cpp(MOC_HEADERS include/ui/Theme.h include/ui/ThemeManager.h + include/notifications/Manager.h + include/AvatarProvider.h include/Cache.h include/ChatPage.h @@ -386,7 +389,7 @@ elseif(WIN32) target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain) else() add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) - target_link_libraries (nheko ${NHEKO_LIBS}) + target_link_libraries (nheko ${NHEKO_LIBS} Qt5::DBus) endif() if(EXTERNAL_PROJECT_DEPS) diff --git a/include/ChatPage.h b/include/ChatPage.h index 3b1562b3..39fb7565 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h @@ -30,6 +30,7 @@ #include "CommunitiesList.h" #include "Community.h" #include "MatrixClient.h" +#include "notifications/Manager.h" class OverlayModal; class QuickSwitcher; @@ -42,6 +43,7 @@ class TopRoomBar; class TypingDisplay; class UserInfoWidget; class UserSettings; +class NotificationsManager; namespace dialogs { class ReadReceipts; @@ -140,6 +142,13 @@ signals: void syncTopBar(const std::map &updates); void dropToLoginPageCb(const QString &msg); + void notifyMessage(const QString &roomid, + const QString &eventid, + const QString &roomname, + const QString &sender, + const QString &message, + const QImage &icon); + private slots: void showUnreadMessageNotification(int count); void updateTopBarAvatar(const QString &roomid, const QPixmap &img); @@ -238,6 +247,8 @@ private: // Global user settings. QSharedPointer userSettings_; + + NotificationsManager notificationsManager; }; template diff --git a/include/notifications/Manager.h b/include/notifications/Manager.h index 4ee4cb98..4ac60097 100644 --- a/include/notifications/Manager.h +++ b/include/notifications/Manager.h @@ -1,12 +1,55 @@ #pragma once #include +#include #include -class NotificationsManager +#if defined(Q_OS_LINUX) +#include +#include +#endif + +struct roomEventId { -public: - static void postNotification(const QString &room, - const QString &user, - const QString &message); + 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) +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) +QDBusArgument & +operator<<(QDBusArgument &arg, const QImage &image); +const QDBusArgument & +operator>>(const QDBusArgument &arg, QImage &); +#endif diff --git a/src/ChatPage.cc b/src/ChatPage.cc index 29747fbe..336ea7c3 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -56,6 +56,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) : QWidget(parent) , isConnected_(true) , userSettings_{userSettings} + , notificationsManager(this) { setObjectName("chatPage"); @@ -541,6 +542,15 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) room_list_->setRoomFilter(communities_[communityId]->getRoomList()); }); + connect(¬ificationsManager, + &NotificationsManager::notificationClicked, + this, + [this](const QString &roomid, const QString &eventid) { + Q_UNUSED(eventid) + room_list_->highlightSelectedRoom(roomid); + activateWindow(); + }); + setGroupViewState(userSettings_->isGroupViewEnabled()); connect(userSettings_.data(), @@ -998,11 +1008,14 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) if (isRoomActive(room_id)) continue; - NotificationsManager::postNotification( + notificationsManager.postNotification( + room_id, + QString::fromStdString(event_id), QString::fromStdString( cache::client()->singleRoomInfo(item.room_id).name), Cache::displayName(room_id, user_id), - utils::event_body(item.event)); + utils::event_body(item.event), + cache::client()->getRoomAvatar(room_id)); } } catch (const lmdb::error &e) { nhlog::db()->warn("error while sending desktop notification: {}", e.what()); diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp index a913128e..80fdb9d8 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp @@ -1,7 +1,158 @@ #include "notifications/Manager.h" -void -NotificationsManager::postNotification(const QString &, const QString &, const QString &) +#include +#include +#include +#include +#include + +NotificationsManager::NotificationsManager(QObject *parent) : + QObject(parent), + dbus( + "org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + QDBusConnection::sessionBus(), + this) { - // TODO: To be implemented + qDBusRegisterMetaType(); + + //connectSlot("ActionInvoked", SLOT(actionInvoked(uint, QString))); + //connectSlot("NotificationClosed", SLOT(notificationClosed(uint, uint))); + 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 << "nheko"; //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)0; // 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(); + } + return true; +} + +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.byteCount()); + 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 index 48fb46ca..66ef713f 100644 --- a/src/notifications/ManagerMac.mm +++ b/src/notifications/ManagerMac.mm @@ -7,16 +7,42 @@ - (void)set_identityImage:(NSImage *)image; @end -void -NotificationsManager::postNotification(const QString &roomName, const QString &userName, const QString &message) +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(userName).toNSString(); - notif.informativeText = message.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 index 7503e852..90367d9a 100644 --- a/src/notifications/ManagerWin.cpp +++ b/src/notifications/ManagerWin.cpp @@ -27,15 +27,25 @@ init() } } +NotificationsManager::NotificationsManager(QObject *parent): QObject(parent) +{ + +} + void -NotificationsManager::postNotification(const QString &room, const QString &user, const QString &msg) +NotificationsManager::postNotification(const QString &, //roomid + const QString &, //eventid + const QString &roomname, + const QString &sender, + const QString &text, + const QImage &) //icon { if (!isInitialized) init(); auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02); - if (room != user) - templ.setTextField(QString("%1 - %2").arg(user).arg(room).toStdWString(), + if (roomname != sender) + templ.setTextField(QString("%1 - %2").arg(sender).arg(roomname).toStdWString(), WinToastTemplate::FirstLine); else templ.setTextField(QString("%1").arg(user).toStdWString(), @@ -46,3 +56,14 @@ NotificationsManager::postNotification(const QString &room, const QString &user, WinToast::instance()->showToast(templ, new CustomHandler()); } + +//unused +void +NotificationsManager::actionInvoked(uint, QString) +{ +} + +void +NotificationsManager::notificationClosed(uint, uint) +{ +}