diff --git a/CMakeLists.txt b/CMakeLists.txt index cf5009699..18e692688 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -369,6 +369,8 @@ set(${PROJECT_NAME}_SOURCES src/model/chathistory.cpp src/model/toxclientstandards.h src/model/ibootstraplistgenerator.h + src/model/notificationgenerator.h + src/model/notificationgenerator.cpp src/net/bootstrapnodeupdater.cpp src/net/bootstrapnodeupdater.h src/net/avatarbroadcaster.cpp diff --git a/cmake/Testing.cmake b/cmake/Testing.cmake index d92674e39..40052b8db 100644 --- a/cmake/Testing.cmake +++ b/cmake/Testing.cmake @@ -48,6 +48,7 @@ auto_test(model groupmessagedispatcher) auto_test(model messageprocessor) auto_test(model sessionchatlog) auto_test(model exiftransform) +auto_test(model notificationgenerator) if (UNIX) auto_test(platform posixsignalnotifier) diff --git a/src/main.cpp b/src/main.cpp index dada33c57..552d16b05 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -94,6 +94,16 @@ void logMessageHandler(QtMsgType type, const QMessageLogContext& ctxt, const QSt && msg == QString("QFSFileEngine::open: No file name specified")) return; + QRegExp snoreFilter{QStringLiteral("Snore::Notification.*was already closed")}; + if (type == QtWarningMsg + && msg.contains(snoreFilter)) + { + // snorenotify logs this when we call requestCloseNotification correctly. The behaviour still works, so we'll + // just mask the warning for now. The issue has been reported upstream: + // https://github.com/qTox/qTox/pull/6073#pullrequestreview-420748519 + return; + } + QString file = ctxt.file; // We're not using QT_MESSAGELOG_FILE here, because that can be 0, NULL, or // nullptr in release builds. diff --git a/src/model/notificationdata.h b/src/model/notificationdata.h new file mode 100644 index 000000000..98d83f5aa --- /dev/null +++ b/src/model/notificationdata.h @@ -0,0 +1,30 @@ +/* + Copyright © 2020 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#pragma once + +#include +#include + +struct NotificationData +{ + QString title; + QString message; + QPixmap pixmap; +}; diff --git a/src/model/notificationgenerator.cpp b/src/model/notificationgenerator.cpp new file mode 100644 index 000000000..7e0ee49a7 --- /dev/null +++ b/src/model/notificationgenerator.cpp @@ -0,0 +1,267 @@ +/* + Copyright © 2020 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#include "notificationgenerator.h" +#include "src/chatlog/content/filetransferwidget.h" + +#include + +namespace +{ + size_t getNumMessages( + const QHash& friendNotifications, + const QHash& groupNotifications) + { + auto numMessages = std::accumulate(friendNotifications.begin(), friendNotifications.end(), 0); + numMessages = std::accumulate(groupNotifications.begin(), groupNotifications.end(), numMessages); + + return numMessages; + } + + size_t getNumChats( + const QHash& friendNotifications, + const QHash& groupNotifications) + { + return friendNotifications.size() + groupNotifications.size(); + } + + QString generateMultiChatTitle(size_t numChats, size_t numMessages) + { + //: e.g. 3 messages from 2 chats + return QObject::tr("%1 message(s) from %2 chats") + .arg(numMessages) + .arg(numChats); + } + + template + QString generateSingleChatTitle( + const QHash numNotifications, + T contact) + { + if (numNotifications[contact] > 1) + { + //: e.g. 2 messages from Bob + return QObject::tr("%1 message(s) from %2") + .arg(numNotifications[contact]) + .arg(contact->getDisplayedName()); + } + else + { + return contact->getDisplayedName(); + } + } + + QString generateTitle( + const QHash& friendNotifications, + const QHash& groupNotifications, + const Friend* f) + { + auto numChats = getNumChats(friendNotifications, groupNotifications); + if (numChats > 1) + { + return generateMultiChatTitle(numChats, getNumMessages(friendNotifications, groupNotifications)); + } + else + { + return generateSingleChatTitle(friendNotifications, f); + } + } + + QString generateTitle( + const QHash& friendNotifications, + const QHash& groupNotifications, + const Group* g) + { + auto numChats = getNumChats(friendNotifications, groupNotifications); + if (numChats > 1) + { + return generateMultiChatTitle(numChats, getNumMessages(friendNotifications, groupNotifications)); + } + else + { + return generateSingleChatTitle(groupNotifications, g); + } + } + + QString generateContent( + const QHash& friendNotifications, + const QHash& groupNotifications, + QString lastMessage, + const ToxPk& sender) + { + assert(friendNotifications.size() > 0 || groupNotifications.size() > 0); + + auto numChats = getNumChats(friendNotifications, groupNotifications); + if (numChats > 1) { + // Copy all names into a vector to simplify formatting logic between + // multiple lists + std::vector displayNames; + displayNames.reserve(numChats); + + for (auto it = friendNotifications.begin(); it != friendNotifications.end(); ++it) { + displayNames.push_back(it.key()->getDisplayedName()); + } + + for (auto it = groupNotifications.begin(); it != groupNotifications.end(); ++it) { + displayNames.push_back(it.key()->getDisplayedName()); + } + + assert(displayNames.size() > 0); + + // Lexiographically sort all display names to ensure consistent formatting + QCollator collator; + std::sort(displayNames.begin(), displayNames.end(), [&] (const QString& a, const QString& b) { + return collator.compare(a, b) < 1; + }); + + auto it = displayNames.begin(); + + QString ret = *it; + + while (++it != displayNames.end()) { + ret += ", " + *it; + } + + return ret; + } + else { + if (groupNotifications.size() == 1) { + return groupNotifications.begin().key()->getPeerList()[sender] + ": " + lastMessage; + } + + return lastMessage; + } + } + + QPixmap getSenderAvatar(Profile* profile, const ToxPk& sender) + { + return profile ? profile->loadAvatar(sender) : QPixmap(); + } +} // namespace + +NotificationGenerator::NotificationGenerator( + INotificationSettings const& notificationSettings, + Profile* profile) + : notificationSettings(notificationSettings) + , profile(profile) +{} + +NotificationData NotificationGenerator::friendMessageNotification(const Friend* f, const QString& message) +{ + friendNotifications[f]++; + + NotificationData ret; + + if (notificationSettings.getNotifyHide()) { + ret.title = tr("New message"); + return ret; + } + + ret.title = generateTitle(friendNotifications, groupNotifications, f); + ret.message = generateContent(friendNotifications, groupNotifications, message, f->getPublicKey()); + ret.pixmap = getSenderAvatar(profile, f->getPublicKey()); + + return ret; +} + +NotificationData NotificationGenerator::groupMessageNotification(const Group* g, const ToxPk& sender, const QString& message) +{ + groupNotifications[g]++; + + NotificationData ret; + + if (notificationSettings.getNotifyHide()){ + ret.title = tr("New group message"); + return ret; + } + + ret.title = generateTitle(friendNotifications, groupNotifications, g); + ret.message = generateContent(friendNotifications, groupNotifications, message, sender); + ret.pixmap = getSenderAvatar(profile, sender); + + return ret; +} + +NotificationData NotificationGenerator::fileTransferNotification(const Friend* f, const QString& filename, size_t fileSize) +{ + friendNotifications[f]++; + + NotificationData ret; + + if (notificationSettings.getNotifyHide()) { + ret.title = tr("Incoming file transfer"); + return ret; + } + + auto numChats = getNumChats(friendNotifications, groupNotifications); + auto numMessages = getNumMessages(friendNotifications, groupNotifications); + + if (numChats > 1 || numMessages > 1) + { + ret.title = generateTitle(friendNotifications, groupNotifications, f); + ret.message = generateContent(friendNotifications, groupNotifications, tr("Incoming file transfer"), f->getPublicKey()); + } + else + { + //: e.g. Bob - file transfer + ret.title = tr("%1 - file transfer").arg(f->getDisplayedName()); + ret.message = filename + " (" + FileTransferWidget::getHumanReadableSize(fileSize) + ")"; + } + + ret.pixmap = getSenderAvatar(profile, f->getPublicKey()); + + return ret; +} + +NotificationData NotificationGenerator::groupInvitationNotification(const Friend* from) +{ + NotificationData ret; + + if (notificationSettings.getNotifyHide()) { + ret.title = tr("Group invite received"); + return ret; + } + + ret.title = tr("%1 invites you to join a group.").arg(from->getDisplayedName()); + ret.message = ""; + ret.pixmap = getSenderAvatar(profile, from->getPublicKey()); + + return ret; +} + +NotificationData NotificationGenerator::friendRequestNotification(const ToxPk& sender, const QString& message) +{ + NotificationData ret; + + if (notificationSettings.getNotifyHide()) { + ret.title = tr("Friend request received"); + return ret; + } + + ret.title = tr("Friend request received from %1").arg(sender.toString()); + ret.message = message; + + return ret; +} + +void NotificationGenerator::onNotificationActivated() +{ + friendNotifications = {}; + groupNotifications = {}; +} diff --git a/src/model/notificationgenerator.h b/src/model/notificationgenerator.h new file mode 100644 index 000000000..d21b6f38f --- /dev/null +++ b/src/model/notificationgenerator.h @@ -0,0 +1,57 @@ +/* + Copyright © 2020 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#pragma once + + +#include "notificationdata.h" +#include "friend.h" +#include "group.h" + +#include "src/persistence/inotificationsettings.h" +#include "src/persistence/profile.h" + +#include +#include + +class NotificationGenerator : public QObject +{ +public: + NotificationGenerator( + INotificationSettings const& notificationSettings, + // Optional profile input to lookup avatars. Avatar lookup is not + // currently mockable so we allow profile to be nullptr for unit + // testing + Profile* profile); + + NotificationData friendMessageNotification(const Friend* f, const QString& message); + NotificationData groupMessageNotification(const Group* g, const ToxPk& sender, const QString& message); + NotificationData fileTransferNotification(const Friend* f, const QString& filename, size_t fileSize); + NotificationData groupInvitationNotification(const Friend* from); + NotificationData friendRequestNotification(const ToxPk& sender, const QString& message); + +public slots: + void onNotificationActivated(); + +private: + INotificationSettings const& notificationSettings; + Profile* profile; + QHash friendNotifications; + QHash groupNotifications; +}; diff --git a/src/persistence/igroupsettings.h b/src/persistence/igroupsettings.h index 1c283822b..158fc484c 100644 --- a/src/persistence/igroupsettings.h +++ b/src/persistence/igroupsettings.h @@ -27,6 +27,4 @@ public: virtual ~IGroupSettings() = default; virtual QStringList getBlackList() const = 0; virtual void setBlackList(const QStringList& blist) = 0; - virtual bool getGroupAlwaysNotify() const = 0; - virtual void setGroupAlwaysNotify(bool newValue) = 0; }; diff --git a/src/persistence/inotificationsettings.h b/src/persistence/inotificationsettings.h new file mode 100644 index 000000000..cdaa87d14 --- /dev/null +++ b/src/persistence/inotificationsettings.h @@ -0,0 +1,47 @@ +/* + Copyright © 2020 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#pragma once + +#include + +class INotificationSettings +{ +public: + virtual bool getNotify() const = 0; + virtual void setNotify(bool newValue) = 0; + + virtual bool getShowWindow() const = 0; + virtual void setShowWindow(bool newValue) = 0; + + virtual bool getDesktopNotify() const = 0; + virtual void setDesktopNotify(bool enabled) = 0; + + virtual bool getNotifySound() const = 0; + virtual void setNotifySound(bool newValue) = 0; + + virtual bool getNotifyHide() const = 0; + virtual void setNotifyHide(bool newValue) = 0; + + virtual bool getBusySound() const = 0; + virtual void setBusySound(bool newValue) = 0; + + virtual bool getGroupAlwaysNotify() const = 0; + virtual void setGroupAlwaysNotify(bool newValue) = 0; +}; diff --git a/src/persistence/settings.h b/src/persistence/settings.h index 08f833304..17fdfa4fe 100644 --- a/src/persistence/settings.h +++ b/src/persistence/settings.h @@ -27,6 +27,7 @@ #include "src/persistence/paths.h" #include "src/persistence/ifriendsettings.h" #include "src/persistence/igroupsettings.h" +#include "src/persistence/inotificationsettings.h" #include "src/video/ivideosettings.h" #include @@ -50,7 +51,8 @@ class Settings : public QObject, public IFriendSettings, public IGroupSettings, public IAudioSettings, - public IVideoSettings + public IVideoSettings, + public INotificationSettings { Q_OBJECT @@ -311,23 +313,23 @@ public: bool getCheckUpdates() const; void setCheckUpdates(bool newValue); - bool getNotify() const; - void setNotify(bool newValue); + bool getNotify() const override; + void setNotify(bool newValue) override; - bool getShowWindow() const; - void setShowWindow(bool newValue); + bool getShowWindow() const override; + void setShowWindow(bool newValue) override; - bool getDesktopNotify() const; - void setDesktopNotify(bool enabled); + bool getDesktopNotify() const override; + void setDesktopNotify(bool enabled) override; - bool getNotifySound() const; - void setNotifySound(bool newValue); + bool getNotifySound() const override; + void setNotifySound(bool newValue) override; - bool getNotifyHide() const; - void setNotifyHide(bool newValue); + bool getNotifyHide() const override; + void setNotifyHide(bool newValue) override; - bool getBusySound() const; - void setBusySound(bool newValue); + bool getBusySound() const override; + void setBusySound(bool newValue) override; bool getGroupAlwaysNotify() const override; void setGroupAlwaysNotify(bool newValue) override; diff --git a/src/platform/desktop_notifications/desktopnotify.cpp b/src/platform/desktop_notifications/desktopnotify.cpp index 59b88dd38..1f4f24d1a 100644 --- a/src/platform/desktop_notifications/desktopnotify.cpp +++ b/src/platform/desktop_notifications/desktopnotify.cpp @@ -25,6 +25,7 @@ #include #include +#include DesktopNotify::DesktopNotify() : notifyCore{Snore::SnoreCore::instance()} @@ -37,43 +38,52 @@ DesktopNotify::DesktopNotify() snoreApp = Snore::Application("qTox", snoreIcon); notifyCore.registerApplication(snoreApp); + + connect(¬ifyCore, &Snore::SnoreCore::notificationClosed, this, &DesktopNotify::onNotificationClose); } -void DesktopNotify::createNotification(const QString& title, const QString& text, Snore::Icon& icon) +void DesktopNotify::notifyMessage(const NotificationData& notificationData) { const Settings& s = Settings::getInstance(); if(!(s.getNotify() && s.getDesktopNotify())) { return; } - Snore::Notification notify{snoreApp, Snore::Alert(), title, text, icon}; + auto icon = notificationData.pixmap.isNull() ? snoreIcon : Snore::Icon(notificationData.pixmap); + auto newNotification = Snore::Notification{snoreApp, Snore::Alert(), notificationData.title, notificationData.message, icon, 0}; + latestId = newNotification.id(); - notifyCore.broadcastNotification(notify); -} - -void DesktopNotify::notifyMessage(const QString& title, const QString& message) -{ - createNotification(title, message, snoreIcon); -} - -void DesktopNotify::notifyMessagePixmap(const QString& title, const QString& message, QPixmap avatar) -{ - Snore::Icon new_icon(avatar); - createNotification(title, message, new_icon); -} - -void DesktopNotify::notifyMessageSimple(const MessageType type) -{ - QString message; - switch (type) { - case MessageType::FRIEND: message = tr("New message"); break; - case MessageType::FRIEND_FILE: message = tr("Incoming file transfer"); break; - case MessageType::FRIEND_REQUEST: message = tr("Friend request received"); break; - case MessageType::GROUP: message = tr("New group message"); break; - case MessageType::GROUP_INVITE: message = tr("Group invite received"); break; - default: break; + if (lastNotification.isValid()) { + // Workaround for broken updating behavior in snore. Snore increments + // the message count when a notification is updated. Snore also caps the + // number of outgoing messages at 3. This means that if we update + // notifications more than 3 times we do not get notifications until the + // user activates the notification. + // + // We work around this by closing the existing notification and replacing + // it with a new one. We then only process the notification close if the + // latest notification id is the same as the one we are closing. This allows + // us to continue counting how many unread messages a user has until they + // close the notification themselves. + // + // I've filed a bug on the snorenotify mailing list but the project seems + // pretty dead. I filed a ticket on March 11 2020, and as of April 5 2020 + // the moderators have not even acknowledged the message. A previous message + // got a response starting with "Snorenotify isn't that well maintained any more" + // (see https://mail.kde.org/pipermail/snorenotify/2019-March/000004.html) + // so I don't have hope of this being fixed any time soon + notifyCore.requestCloseNotification(lastNotification, Snore::Notification::CloseReasons::Dismissed); } - createNotification(message, {}, snoreIcon); + notifyCore.broadcastNotification(newNotification); + lastNotification = newNotification; +} + +void DesktopNotify::onNotificationClose(Snore::Notification notification) +{ + if (notification.id() == latestId) { + lastNotification = {}; + emit notificationClosed(); + } } #endif diff --git a/src/platform/desktop_notifications/desktopnotify.h b/src/platform/desktop_notifications/desktopnotify.h index cd6baedad..f162de3c3 100644 --- a/src/platform/desktop_notifications/desktopnotify.h +++ b/src/platform/desktop_notifications/desktopnotify.h @@ -19,11 +19,14 @@ #pragma once -#if DESKTOP_NOTIFICATIONS +#include "src/model/notificationdata.h" + #include #include + #include +#include class DesktopNotify : public QObject { @@ -31,25 +34,19 @@ class DesktopNotify : public QObject public: DesktopNotify(); - enum class MessageType { - FRIEND, - FRIEND_FILE, - FRIEND_REQUEST, - GROUP, - GROUP_INVITE - }; - public slots: - void notifyMessage(const QString& title, const QString& message); - void notifyMessagePixmap(const QString& title, const QString& message, QPixmap avatar); - void notifyMessageSimple(const MessageType type); + void notifyMessage(const NotificationData& notificationData); -private: - void createNotification(const QString& title, const QString& text, Snore::Icon& icon); +signals: + void notificationClosed(); + +private slots: + void onNotificationClose(Snore::Notification notification); private: Snore::SnoreCore& notifyCore; Snore::Application snoreApp; Snore::Icon snoreIcon; + Snore::Notification lastNotification; + uint latestId; }; -#endif // DESKTOP_NOTIFICATIONS diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index 798a27905..1f18a3e71 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -294,6 +294,11 @@ void Widget::init() profileInfo = new ProfileInfo(core, profile); profileForm = new ProfileForm(profileInfo); +#if DESKTOP_NOTIFICATIONS + notificationGenerator.reset(new NotificationGenerator(settings, profile)); + connect(¬ifier, &DesktopNotify::notificationClosed, notificationGenerator.get(), &NotificationGenerator::onNotificationActivated); +#endif + // connect logout tray menu action connect(actionLogout, &QAction::triggered, profileForm, &ProfileForm::onLogoutClicked); @@ -1498,7 +1503,7 @@ void Widget::addGroupDialog(Group* group, ContentDialog* dialog) emit widget->chatroomWidgetClicked(widget); } -bool Widget::newFriendMessageAlert(const ToxPk& friendId, const QString& text, bool sound, bool file) +bool Widget::newFriendMessageAlert(const ToxPk& friendId, const QString& text, bool sound, QString filename, size_t filesize) { bool hasActive; QWidget* currentWindow; @@ -1535,17 +1540,9 @@ bool Widget::newFriendMessageAlert(const ToxPk& friendId, const QString& text, b widget->updateStatusLight(); ui->friendList->trackWidget(widget); #if DESKTOP_NOTIFICATIONS - if (settings.getNotifyHide()) { - notifier.notifyMessageSimple(file ? DesktopNotify::MessageType::FRIEND_FILE - : DesktopNotify::MessageType::FRIEND); - } else { - QString title = f->getDisplayedName(); - if (file) { - title += " - " + tr("File sent"); - } - notifier.notifyMessagePixmap(title, text, - Nexus::getProfile()->loadAvatar(f->getPublicKey())); - } + auto notificationData = filename.isEmpty() ? notificationGenerator->friendMessageNotification(f, text) + : notificationGenerator->fileTransferNotification(f, filename, filesize); + notifier.notifyMessage(notificationData); #endif if (contentDialog == nullptr) { @@ -1586,18 +1583,8 @@ bool Widget::newGroupMessageAlert(const GroupId& groupId, const ToxPk& authorPk, g->setEventFlag(true); widget->updateStatusLight(); #if DESKTOP_NOTIFICATIONS - if (settings.getNotifyHide()) { - notifier.notifyMessageSimple(DesktopNotify::MessageType::GROUP); - } else { - Friend* f = FriendList::findFriend(authorPk); - QString title = g->getPeerList().value(authorPk) + " (" + g->getDisplayedName() + ")"; - if (!f) { - notifier.notifyMessage(title, message); - } else { - notifier.notifyMessagePixmap(title, message, - Nexus::getProfile()->loadAvatar(f->getPublicKey())); - } - } + auto notificationData = notificationGenerator->groupMessageNotification(g, authorPk, message); + notifier.notifyMessage(notificationData); #endif if (contentDialog == nullptr) { @@ -1673,11 +1660,8 @@ void Widget::onFriendRequestReceived(const ToxPk& friendPk, const QString& messa friendRequestsUpdate(); newMessageAlert(window(), isActiveWindow(), true, true); #if DESKTOP_NOTIFICATIONS - if (settings.getNotifyHide()) { - notifier.notifyMessageSimple(DesktopNotify::MessageType::FRIEND_REQUEST); - } else { - notifier.notifyMessage(friendPk.toString() + tr(" sent you a friend request."), message); - } + auto notificationData = notificationGenerator->friendRequestNotification(friendPk, message); + notifier.notifyMessage(notificationData); #endif } } @@ -1686,9 +1670,8 @@ void Widget::onFileReceiveRequested(const ToxFile& file) { const ToxPk& friendPk = FriendList::id2Key(file.friendId); newFriendMessageAlert(friendPk, - file.fileName + " (" - + FileTransferWidget::getHumanReadableSize(file.filesize) + ")", - true, true); + {}, + true, file.fileName, file.filesize); } void Widget::updateFriendActivity(const Friend& frnd) @@ -1934,12 +1917,8 @@ void Widget::onGroupInviteReceived(const GroupInvite& inviteInfo) groupInvitesUpdate(); newMessageAlert(window(), isActiveWindow(), true, true); #if DESKTOP_NOTIFICATIONS - if (settings.getNotifyHide()) { - notifier.notifyMessageSimple(DesktopNotify::MessageType::GROUP_INVITE); - } else { - notifier.notifyMessagePixmap(f->getDisplayedName() + tr(" invites you to join a group."), - {}, Nexus::getProfile()->loadAvatar(f->getPublicKey())); - } + auto notificationData = notificationGenerator->groupInvitationNotification(f); + notifier.notifyMessage(notificationData); #endif } } else { diff --git a/src/widget/widget.h b/src/widget/widget.h index 34170a4ee..482889a3b 100644 --- a/src/widget/widget.h +++ b/src/widget/widget.h @@ -38,6 +38,7 @@ #include "src/model/friendmessagedispatcher.h" #include "src/model/groupmessagedispatcher.h" #if DESKTOP_NOTIFICATIONS +#include "src/model/notificationgenerator.h" #include "src/platform/desktop_notifications/desktopnotify.h" #endif @@ -127,7 +128,7 @@ public: void addFriendDialog(const Friend* frnd, ContentDialog* dialog); void addGroupDialog(Group* group, ContentDialog* dialog); bool newFriendMessageAlert(const ToxPk& friendId, const QString& text, bool sound = true, - bool file = false); + QString filename = QString(), size_t filesize = 0); bool newGroupMessageAlert(const GroupId& groupId, const ToxPk& authorPk, const QString& message, bool notify); bool getIsWindowMinimized(); @@ -360,6 +361,7 @@ private: MessageProcessor::SharedParams sharedMessageProcessorParams; #if DESKTOP_NOTIFICATIONS + std::unique_ptr notificationGenerator; DesktopNotify notifier; #endif diff --git a/test/mock/mockcoreidhandler.h b/test/mock/mockcoreidhandler.h new file mode 100644 index 000000000..8aad8117a --- /dev/null +++ b/test/mock/mockcoreidhandler.h @@ -0,0 +1,45 @@ +/* + Copyright © 2020 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#pragma once + +#include "src/core/icoreidhandler.h" + +#include + +class MockCoreIdHandler : public ICoreIdHandler +{ +public: + ToxId getSelfId() const override + { + std::terminate(); + return ToxId(); + } + + ToxPk getSelfPublicKey() const override + { + static uint8_t id[TOX_PUBLIC_KEY_SIZE] = {0}; + return ToxPk(id); + } + + QString getUsername() const override + { + return "me"; + } +}; diff --git a/test/mock/mockgroupquery.h b/test/mock/mockgroupquery.h new file mode 100644 index 000000000..b039c7cbc --- /dev/null +++ b/test/mock/mockgroupquery.h @@ -0,0 +1,82 @@ +/* + Copyright © 2020 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#pragma once + +#include "src/core/icoregroupquery.h" + +#include + +/** + * Mock 1 peer at group number 0 + */ +class MockGroupQuery : public ICoreGroupQuery +{ +public: + GroupId getGroupPersistentId(uint32_t groupNumber) const override + { + return GroupId(0); + } + + uint32_t getGroupNumberPeers(int groupId) const override + { + if (emptyGroup) { + return 1; + } + + return 2; + } + + QString getGroupPeerName(int groupId, int peerId) const override + { + return QString("peer") + peerId; + } + + ToxPk getGroupPeerPk(int groupId, int peerId) const override + { + uint8_t id[TOX_PUBLIC_KEY_SIZE] = {static_cast(peerId)}; + return ToxPk(id); + } + + QStringList getGroupPeerNames(int groupId) const override + { + if (emptyGroup) { + return QStringList({QString("me")}); + } + return QStringList({QString("me"), QString("other")}); + } + + bool getGroupAvEnabled(int groupId) const override + { + return false; + } + + void setAsEmptyGroup() + { + emptyGroup = true; + } + + void setAsFunctionalGroup() + { + emptyGroup = false; + } + +private: + bool emptyGroup = false; +}; diff --git a/test/model/groupmessagedispatcher_test.cpp b/test/model/groupmessagedispatcher_test.cpp index e8d0372d4..cdebc64b1 100644 --- a/test/model/groupmessagedispatcher_test.cpp +++ b/test/model/groupmessagedispatcher_test.cpp @@ -23,6 +23,9 @@ #include "src/model/message.h" #include "src/persistence/settings.h" +#include "test/mock/mockcoreidhandler.h" +#include "test/mock/mockgroupquery.h" + #include #include @@ -47,85 +50,6 @@ public: size_t numSentMessages = 0; }; -/** - * Mock 1 peer at group number 0 - */ -class MockGroupQuery : public ICoreGroupQuery -{ -public: - GroupId getGroupPersistentId(uint32_t groupNumber) const override - { - return GroupId(); - } - - uint32_t getGroupNumberPeers(int groupId) const override - { - if (emptyGroup) { - return 1; - } - - return 2; - } - - QString getGroupPeerName(int groupId, int peerId) const override - { - return QString("peer") + peerId; - } - - ToxPk getGroupPeerPk(int groupId, int peerId) const override - { - uint8_t id[TOX_PUBLIC_KEY_SIZE] = {static_cast(peerId)}; - return ToxPk(id); - } - - QStringList getGroupPeerNames(int groupId) const override - { - if (emptyGroup) { - return QStringList({QString("me")}); - } - return QStringList({QString("me"), QString("other")}); - } - - bool getGroupAvEnabled(int groupId) const override - { - return false; - } - - void setAsEmptyGroup() - { - emptyGroup = true; - } - - void setAsFunctionalGroup() - { - emptyGroup = false; - } - -private: - bool emptyGroup = false; -}; - -class MockCoreIdHandler : public ICoreIdHandler -{ -public: - ToxId getSelfId() const override - { - std::terminate(); - return ToxId(); - } - - ToxPk getSelfPublicKey() const override - { - static uint8_t id[TOX_PUBLIC_KEY_SIZE] = {0}; - return ToxPk(id); - } - - QString getUsername() const override - { - return "me"; - } -}; - class MockGroupSettings : public IGroupSettings { public: @@ -139,13 +63,6 @@ public: blacklist = blist; } - bool getGroupAlwaysNotify() const override - { - return false; - } - - void setGroupAlwaysNotify(bool newValue) override {} - private: QStringList blacklist; }; diff --git a/test/model/notificationgenerator_test.cpp b/test/model/notificationgenerator_test.cpp new file mode 100644 index 000000000..b1135a688 --- /dev/null +++ b/test/model/notificationgenerator_test.cpp @@ -0,0 +1,371 @@ + +/* + Copyright © 2019 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#include "src/model/notificationgenerator.h" + +#include "test/mock/mockcoreidhandler.h" +#include "test/mock/mockgroupquery.h" + +#include +#include + +namespace +{ + class MockNotificationSettings : public INotificationSettings + { + virtual bool getNotify() const override { return true; } + + virtual void setNotify(bool newValue) override {} + + virtual bool getShowWindow() const override { return true; } + virtual void setShowWindow(bool newValue) override {} + + virtual bool getDesktopNotify() const override { return true; } + virtual void setDesktopNotify(bool enabled) override {} + + virtual bool getNotifySound() const override { return true; } + virtual void setNotifySound(bool newValue) override {} + + virtual bool getNotifyHide() const override { return notifyHide; } + virtual void setNotifyHide(bool newValue) override { notifyHide = newValue; }; + + virtual bool getBusySound() const override { return true; } + virtual void setBusySound(bool newValue) override {} + + virtual bool getGroupAlwaysNotify() const override { return true; } + virtual void setGroupAlwaysNotify(bool newValue) override {} + private: + bool notifyHide = false; + }; + +} // namespace + +class TestNotificationGenerator : public QObject +{ + Q_OBJECT + +private slots: + void init(); + void testSingleFriendMessage(); + void testMultipleFriendMessages(); + void testNotificationClear(); + void testGroupMessage(); + void testMultipleGroupMessages(); + void testMultipleFriendSourceMessages(); + void testMultipleGroupSourceMessages(); + void testMixedSourceMessages(); + void testFileTransfer(); + void testFileTransferAfterMessage(); + void testGroupInvitation(); + void testGroupInviteUncounted(); + void testFriendRequest(); + void testFriendRequestUncounted(); + void testSimpleFriendMessage(); + void testSimpleFileTransfer(); + void testSimpleGroupMessage(); + void testSimpleFriendRequest(); + void testSimpleGroupInvite(); + void testSimpleMessageToggle(); + +private: + std::unique_ptr notificationSettings; + std::unique_ptr notificationGenerator; + std::unique_ptr groupQuery; + std::unique_ptr coreIdHandler; +}; + +void TestNotificationGenerator::init() +{ + notificationSettings.reset(new MockNotificationSettings()); + notificationGenerator.reset(new NotificationGenerator(*notificationSettings, nullptr)); + groupQuery.reset(new MockGroupQuery()); + coreIdHandler.reset(new MockCoreIdHandler()); +} + +void TestNotificationGenerator::testSingleFriendMessage() +{ + Friend f(0, ToxPk()); + f.setName("friendName"); + auto notificationData = notificationGenerator->friendMessageNotification(&f, "test"); + QVERIFY(notificationData.title == "friendName"); + QVERIFY(notificationData.message == "test"); +} + +void TestNotificationGenerator::testMultipleFriendMessages() +{ + Friend f(0, ToxPk()); + f.setName("friendName"); + notificationGenerator->friendMessageNotification(&f, "test"); + auto notificationData = notificationGenerator->friendMessageNotification(&f, "test2"); + QVERIFY(notificationData.title == "2 message(s) from friendName"); + QVERIFY(notificationData.message == "test2"); + + notificationData = notificationGenerator->friendMessageNotification(&f, "test3"); + QVERIFY(notificationData.title == "3 message(s) from friendName"); + QVERIFY(notificationData.message == "test3"); +} + +void TestNotificationGenerator::testNotificationClear() +{ + Friend f(0, ToxPk()); + f.setName("friendName"); + + notificationGenerator->friendMessageNotification(&f, "test"); + + // On notification clear we shouldn't see a notification count from the friend + notificationGenerator->onNotificationActivated(); + + auto notificationData = notificationGenerator->friendMessageNotification(&f, "test2"); + QVERIFY(notificationData.title == "friendName"); + QVERIFY(notificationData.message == "test2"); +} + +void TestNotificationGenerator::testGroupMessage() +{ + Group g(0, GroupId(0), "groupName", false, "selfName", *groupQuery, *coreIdHandler); + auto sender = groupQuery->getGroupPeerPk(0, 0); + g.updateUsername(sender, "sender1"); + + auto notificationData = notificationGenerator->groupMessageNotification(&g, sender, "test"); + QVERIFY(notificationData.title == "groupName"); + QVERIFY(notificationData.message == "sender1: test"); +} + +void TestNotificationGenerator::testMultipleGroupMessages() +{ + Group g(0, GroupId(0), "groupName", false, "selfName", *groupQuery, *coreIdHandler); + + auto sender = groupQuery->getGroupPeerPk(0, 0); + g.updateUsername(sender, "sender1"); + + auto sender2 = groupQuery->getGroupPeerPk(0, 1); + g.updateUsername(sender2, "sender2"); + + notificationGenerator->groupMessageNotification(&g, sender, "test1"); + + auto notificationData = notificationGenerator->groupMessageNotification(&g, sender2, "test2"); + QVERIFY(notificationData.title == "2 message(s) from groupName"); + QVERIFY(notificationData.message == "sender2: test2"); +} + +void TestNotificationGenerator::testMultipleFriendSourceMessages() +{ + Friend f(0, ToxPk()); + f.setName("friend1"); + + Friend f2(1, ToxPk()); + f2.setName("friend2"); + + notificationGenerator->friendMessageNotification(&f, "test1"); + auto notificationData = notificationGenerator->friendMessageNotification(&f2, "test2"); + + QVERIFY(notificationData.title == "2 message(s) from 2 chats"); + QVERIFY(notificationData.message == "friend1, friend2"); +} + +void TestNotificationGenerator::testMultipleGroupSourceMessages() +{ + Group g(0, GroupId(QByteArray(32, 0)), "groupName", false, "selfName", *groupQuery, *coreIdHandler); + Group g2(1, GroupId(QByteArray(32, 1)), "groupName2", false, "selfName", *groupQuery, *coreIdHandler); + + auto sender = groupQuery->getGroupPeerPk(0, 0); + g.updateUsername(sender, "sender1"); + + notificationGenerator->groupMessageNotification(&g, sender, "test1"); + auto notificationData = notificationGenerator->groupMessageNotification(&g2, sender, "test1"); + + QVERIFY(notificationData.title == "2 message(s) from 2 chats"); + QVERIFY(notificationData.message == "groupName, groupName2"); +} + +void TestNotificationGenerator::testMixedSourceMessages() +{ + Friend f(0, ToxPk()); + f.setName("friend"); + + Group g(0, GroupId(QByteArray(32, 0)), "group", false, "selfName", *groupQuery, *coreIdHandler); + + auto sender = groupQuery->getGroupPeerPk(0, 0); + g.updateUsername(sender, "sender1"); + + notificationGenerator->friendMessageNotification(&f, "test1"); + auto notificationData = notificationGenerator->groupMessageNotification(&g, sender, "test2"); + + QVERIFY(notificationData.title == "2 message(s) from 2 chats"); + QVERIFY(notificationData.message == "friend, group"); + + notificationData = notificationGenerator->fileTransferNotification(&f, "file", 0); + QVERIFY(notificationData.title == "3 message(s) from 2 chats"); + QVERIFY(notificationData.message == "friend, group"); +} + +void TestNotificationGenerator::testFileTransfer() +{ + Friend f(0, ToxPk()); + f.setName("friend"); + + auto notificationData = notificationGenerator->fileTransferNotification(&f, "file", 5 * 1024 * 1024 /* 5MB */); + + QVERIFY(notificationData.title == "friend - file transfer"); + QVERIFY(notificationData.message == "file (5.00MiB)"); +} + +void TestNotificationGenerator::testFileTransferAfterMessage() +{ + Friend f(0, ToxPk()); + f.setName("friend"); + + notificationGenerator->friendMessageNotification(&f, "test1"); + auto notificationData = notificationGenerator->fileTransferNotification(&f, "file", 5 * 1024 * 1024 /* 5MB */); + + QVERIFY(notificationData.title == "2 message(s) from friend"); + QVERIFY(notificationData.message == "Incoming file transfer"); +} + +void TestNotificationGenerator::testGroupInvitation() +{ + Friend f(0, ToxPk()); + f.setName("friend"); + + auto notificationData = notificationGenerator->groupInvitationNotification(&f); + + QVERIFY(notificationData.title == "friend invites you to join a group."); + QVERIFY(notificationData.message == ""); +} + +void TestNotificationGenerator::testGroupInviteUncounted() +{ + Friend f(0, ToxPk()); + f.setName("friend"); + + notificationGenerator->friendMessageNotification(&f, "test"); + notificationGenerator->groupInvitationNotification(&f); + auto notificationData = notificationGenerator->friendMessageNotification(&f, "test2"); + + QVERIFY(notificationData.title == "2 message(s) from friend"); + QVERIFY(notificationData.message == "test2"); +} + +void TestNotificationGenerator::testFriendRequest() +{ + ToxPk sender(QByteArray(32, 0)); + + auto notificationData = notificationGenerator->friendRequestNotification(sender, "request"); + + QVERIFY(notificationData.title == "Friend request received from 0000000000000000000000000000000000000000000000000000000000000000"); + QVERIFY(notificationData.message == "request"); +} + +void TestNotificationGenerator::testFriendRequestUncounted() +{ + Friend f(0, ToxPk()); + f.setName("friend"); + ToxPk sender(QByteArray(32, 0)); + + notificationGenerator->friendMessageNotification(&f, "test"); + notificationGenerator->friendRequestNotification(sender, "request"); + auto notificationData = notificationGenerator->friendMessageNotification(&f, "test2"); + + QVERIFY(notificationData.title == "2 message(s) from friend"); + QVERIFY(notificationData.message == "test2"); +} + +void TestNotificationGenerator::testSimpleFriendMessage() +{ + Friend f(0, ToxPk()); + f.setName("friend"); + + notificationSettings->setNotifyHide(true); + + auto notificationData = notificationGenerator->friendMessageNotification(&f, "test"); + + QVERIFY(notificationData.title == "New message"); + QVERIFY(notificationData.message == ""); +} + +void TestNotificationGenerator::testSimpleFileTransfer() +{ + Friend f(0, ToxPk()); + f.setName("friend"); + + notificationSettings->setNotifyHide(true); + + auto notificationData = notificationGenerator->fileTransferNotification(&f, "file", 0); + + QVERIFY(notificationData.title == "Incoming file transfer"); + QVERIFY(notificationData.message == ""); +} + +void TestNotificationGenerator::testSimpleGroupMessage() +{ + Group g(0, GroupId(0), "groupName", false, "selfName", *groupQuery, *coreIdHandler); + auto sender = groupQuery->getGroupPeerPk(0, 0); + g.updateUsername(sender, "sender1"); + + notificationSettings->setNotifyHide(true); + + auto notificationData = notificationGenerator->groupMessageNotification(&g, sender, "test"); + QVERIFY(notificationData.title == "New group message"); + QVERIFY(notificationData.message == ""); +} + +void TestNotificationGenerator::testSimpleFriendRequest() +{ + ToxPk sender(QByteArray(32, 0)); + + notificationSettings->setNotifyHide(true); + + auto notificationData = notificationGenerator->friendRequestNotification(sender, "request"); + + QVERIFY(notificationData.title == "Friend request received"); + QVERIFY(notificationData.message == ""); +} + +void TestNotificationGenerator::testSimpleGroupInvite() +{ + Friend f(0, ToxPk()); + f.setName("friend"); + + notificationSettings->setNotifyHide(true); + auto notificationData = notificationGenerator->groupInvitationNotification(&f); + + QVERIFY(notificationData.title == "Group invite received"); + QVERIFY(notificationData.message == ""); +} + +void TestNotificationGenerator::testSimpleMessageToggle() +{ + Friend f(0, ToxPk()); + f.setName("friend"); + + notificationSettings->setNotifyHide(true); + + notificationGenerator->friendMessageNotification(&f, "test"); + + notificationSettings->setNotifyHide(false); + + auto notificationData = notificationGenerator->friendMessageNotification(&f, "test2"); + + QVERIFY(notificationData.title == "2 message(s) from friend"); + QVERIFY(notificationData.message == "test2"); +} + +QTEST_GUILESS_MAIN(TestNotificationGenerator) +#include "notificationgenerator_test.moc"