From 2cc82053c9a949c3fe9156ed0f7ae7f27290654d Mon Sep 17 00:00:00 2001 From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> Date: Thu, 1 Apr 2021 18:58:05 -0400 Subject: [PATCH] notifications: implement libnotify on gnu/linux Gitlab: #320 Change-Id: I39ffd7e0ba8c5160e81a6b64260f3d92ab8c619a --- CMakeLists.txt | 14 ++- src/accountadapter.cpp | 4 +- src/calladapter.cpp | 135 +++++++++++++++++------- src/calladapter.h | 7 +- src/conversationsadapter.cpp | 140 +++++++++++++++++------- src/conversationsadapter.h | 5 + src/lrcinstance.h | 39 +++++++ src/systemtray.cpp | 199 ++++++++++++++++++++++++++++++++++- src/systemtray.h | 23 ++++ 9 files changed, 475 insertions(+), 91 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c5543b492..fd3c0b9b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,12 +130,18 @@ set(COMMON_HEADERS ${SRC_DIR}/appsettingsmanager.h) find_package(PkgConfig REQUIRED) -pkg_check_modules(LIBNM libnm) +pkg_check_modules(LIBNM libnm) if(LIBNM_FOUND) add_definitions(-DUSE_LIBNM) endif() +pkg_check_modules(LIBNOTIFY libnotify>=0.7.6) +if(LIBNOTIFY_FOUND) + add_definitions(-DUSE_LIBNOTIFY) + add_definitions(${LIBNOTIFY_CFLAGS}) +endif() + if(QT5_VER AND QT5_PATH) string(REPLACE "." ";" VERSION_LIST ${QT5_VER}) list(GET VERSION_LIST 0 QT5_VER_MAJOR) @@ -210,7 +216,8 @@ message("Will expect lrc headers in ${LRC_SRC_PATH}") include_directories(${PROJECT_SOURCE_DIR} ${SRC_DIR} ${LRC_SRC_PATH} - ${LIBNM_INCLUDE_DIRS}) + ${LIBNM_INCLUDE_DIRS} + ${LIBNOTIFY_INCLUDE_DIRS}) add_executable(${PROJECT_NAME} ${SRC_DIR}/main.cpp @@ -231,7 +238,8 @@ target_link_libraries(jami-qt ${LRC_LIB_NAME} ${qrencode} ${X11} - ${LIBNM_LIBRARIES}) + ${LIBNM_LIBRARIES} + ${LIBNOTIFY_LIBRARIES}) if(ENABLE_TESTS) message("Add Jami tests") diff --git a/src/accountadapter.cpp b/src/accountadapter.cpp index 7be029e71..24fe6e059 100644 --- a/src/accountadapter.cpp +++ b/src/accountadapter.cpp @@ -354,6 +354,7 @@ AccountAdapter::deselectConversation() return; } + // TODO: remove this unhealthy section auto currentConversationModel = lrcInstance_->getCurrentConversationModel(); if (currentConversationModel == nullptr) { @@ -421,8 +422,7 @@ AccountAdapter::connectAccount(const QString& accountId) contactUnbannedConnection_ = QObject::connect(accInfo.contactModel.get(), &lrc::api::ContactModel::bannedStatusChanged, - [this](const QString& contactUri, - bool banned) { + [this](const QString&, bool banned) { if (!banned) Q_EMIT contactUnbanned(); }); diff --git a/src/calladapter.cpp b/src/calladapter.cpp index 96e895675..dc55f0e31 100644 --- a/src/calladapter.cpp +++ b/src/calladapter.cpp @@ -49,6 +49,29 @@ CallAdapter::CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject* &CallAdapter::onShowCallView); connect(lrcInstance_, &LRCInstance::currentAccountChanged, this, &CallAdapter::onAccountChanged); + +#ifdef Q_OS_LINUX + // notification responses (gnu/linux currently) + connect(systemTray_, + &SystemTray::answerCallActivated, + [this](const QString& accountId, const QString& convUid) { + acceptACall(accountId, convUid); + Q_EMIT lrcInstance_->notificationClicked(); + lrcInstance_->selectConversation(accountId, convUid); + updateCall(convUid, accountId); + Q_EMIT callSetupMainViewRequired(accountId, convUid); + }); + connect(systemTray_, + &SystemTray::declineCallActivated, + [this](const QString& accountId, const QString& convUid) { + refuseACall(accountId, convUid); + }); +#endif + + connect(&lrcInstance_->behaviorController(), + &BehaviorController::callStatusChanged, + this, + &CallAdapter::onCallStatusChanged); } void @@ -58,6 +81,39 @@ CallAdapter::onAccountChanged() connectCallModel(accountId_); } +void +CallAdapter::onCallStatusChanged(const QString& accountId, const QString& callId) +{ +#ifdef Q_OS_LINUX + auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); + auto& callModel = accInfo.callModel; + const auto call = callModel->getCall(callId); + + const auto& convInfo = lrcInstance_->getConversationFromCallId(callId, accountId); + if (convInfo.uid.isEmpty()) + return; + + // handle notifications + if (call.status == lrc::api::call::Status::IN_PROGRESS) { + // Call answered and in progress; close the notification + systemTray_->hideNotification(QString("%1;%2").arg(accountId).arg(convInfo.uid)); + } else if (call.status == lrc::api::call::Status::ENDED) { + // Call ended; close the notification + if (systemTray_->hideNotification(QString("%1;%2").arg(accountId).arg(convInfo.uid)) + && call.startTime.time_since_epoch().count() == 0) { + // This was a missed call; show a missed call notification + auto& accInfo = lrcInstance_->getAccountInfo(accountId); + auto from = accInfo.contactModel->bestNameForContact(convInfo.participants[0]); + auto notifId = QString("%1;%2").arg(accountId).arg(convInfo.uid); + systemTray_->showNotification(notifId, + tr("Missed call"), + tr("Missed call from %1").arg(from), + NotificationType::CHAT); + } + } +#endif +} + void CallAdapter::placeAudioOnlyCall() { @@ -207,13 +263,8 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con void CallAdapter::onShowCallView(const QString& accountId, const QString& convUid) { - const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId); - if (convInfo.uid.isEmpty()) { - return; - } - - updateCall(convInfo.uid, accountId); - Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid); + updateCall(convUid, accountId); + Q_EMIT callSetupMainViewRequired(accountId, convUid); } void @@ -330,16 +381,25 @@ CallAdapter::showNotification(const QString& accountId, const QString& convUid) from = accInfo.contactModel->bestNameForContact(convInfo.participants[0]); } + Q_EMIT lrcInstance_->updateSmartList(); + +#ifdef Q_OS_LINUX + auto notifId = QString("%1;%2").arg(accountId).arg(convUid); + systemTray_->showNotification(notifId, + tr("Incoming call"), + tr("%1 is calling you").arg(from), + NotificationType::CALL); +#else auto onClicked = [this, accountId, convUid = convInfo.uid]() { const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId); if (convInfo.uid.isEmpty()) { return; } Q_EMIT lrcInstance_->notificationClicked(); - Q_EMIT callSetupMainViewRequired(convInfo.accountId, convInfo.uid); + Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid); }; - Q_EMIT lrcInstance_->updateSmartList(); systemTray_->showNotification(tr("is calling you"), from, onClicked); +#endif } void @@ -347,33 +407,31 @@ CallAdapter::connectCallModel(const QString& accountId) { auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); - QObject::disconnect(callStatusChangedConnection_); - QObject::disconnect(onParticipantsChangedConnection_); - - onParticipantsChangedConnection_ - = QObject::connect(accInfo.callModel.get(), - &lrc::api::NewCallModel::onParticipantsChanged, - [this, accountId](const QString& confId) { - auto& accInfo = lrcInstance_->accountModel().getAccountInfo( - accountId); - auto& callModel = accInfo.callModel; - auto call = callModel->getCall(confId); - const auto& convInfo = lrcInstance_->getConversationFromCallId( - confId); - if (!convInfo.uid.isEmpty()) { - QVariantList map; - for (const auto& participant : call.participantsInfos) { - QJsonObject data = fillParticipantData(participant); - map.push_back(QVariant(data)); - updateCallOverlay(convInfo); - } - Q_EMIT updateParticipantsInfos(map, accountId, confId); - } - }); - - callStatusChangedConnection_ = QObject::connect( + connect( + accInfo.callModel.get(), + &lrc::api::NewCallModel::onParticipantsChanged, + this, + [this, accountId](const QString& confId) { + auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); + auto& callModel = accInfo.callModel; + auto call = callModel->getCall(confId); + const auto& convInfo = lrcInstance_->getConversationFromCallId(confId); + if (!convInfo.uid.isEmpty()) { + QVariantList map; + for (const auto& participant : call.participantsInfos) { + QJsonObject data = fillParticipantData(participant); + map.push_back(QVariant(data)); + updateCallOverlay(convInfo); + } + Q_EMIT updateParticipantsInfos(map, accountId, confId); + } + }, + Qt::UniqueConnection); + + connect( accInfo.callModel.get(), &lrc::api::NewCallModel::callStatusChanged, + this, [this, accountId](const QString& callId) { auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); auto& callModel = accInfo.callModel; @@ -455,11 +513,13 @@ CallAdapter::connectCallModel(const QString& accountId) default: break; } - }); + }, + Qt::UniqueConnection); - remoteRecordingChangedConnection_ = QObject::connect( + connect( accInfo.callModel.get(), &lrc::api::NewCallModel::remoteRecordingChanged, + this, [this](const QString& callId, const QSet<QString>& peerRec, bool state) { const auto currentCallId = lrcInstance_->getCallIdForConversationUid(convUid_, accountId_); @@ -478,7 +538,8 @@ CallAdapter::connectCallModel(const QString& accountId) Q_EMIT remoteRecordingChanged(peers, false); } } - }); + }, + Qt::UniqueConnection); } void diff --git a/src/calladapter.h b/src/calladapter.h index 0ddd9c2cb..b9a1d2b03 100644 --- a/src/calladapter.h +++ b/src/calladapter.h @@ -103,6 +103,7 @@ public Q_SLOTS: void onShowIncomingCallView(const QString& accountId, const QString& convUid); void onShowCallView(const QString& accountId, const QString& convUid); void onAccountChanged(); + void onCallStatusChanged(const QString& accountId, const QString& callId); private: bool shouldShowPreview(bool force); @@ -113,12 +114,6 @@ private: QString accountId_; QString convUid_; - QMetaObject::Connection callStatusChangedConnection_; - QMetaObject::Connection onParticipantsChangedConnection_; - QMetaObject::Connection closeIncomingCallPageConnection_; - QMetaObject::Connection appStateChangedConnection_; - QMetaObject::Connection remoteRecordingChangedConnection_; - // For Call Overlay void updateCallOverlay(const lrc::api::conversation::Info& convInfo); void setTime(const QString& accountId, const QString& convUid); diff --git a/src/conversationsadapter.cpp b/src/conversationsadapter.cpp index 6f1c46ea1..95ece1e63 100644 --- a/src/conversationsadapter.cpp +++ b/src/conversationsadapter.cpp @@ -38,6 +38,43 @@ ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray, connect(this, &ConversationsAdapter::currentTypeFilterChanged, [this]() { lrcInstance_->getCurrentConversationModel()->setFilter(currentTypeFilter_); }); + + connect(lrcInstance_, &LRCInstance::conversationSelected, [this]() { + auto convUid = lrcInstance_->getCurrentConvUid(); + if (!convUid.isEmpty()) { + Q_EMIT showConversation(lrcInstance_->getCurrAccId(), convUid); + } + }); + +#ifdef Q_OS_LINUX + // notification responses + connect(systemTray_, + &SystemTray::openConversationActivated, + [this](const QString& accountId, const QString& convUid) { + Q_EMIT lrcInstance_->notificationClicked(); + selectConversation(accountId, convUid); + Q_EMIT lrcInstance_->updateSmartList(); + Q_EMIT modelSorted(convUid); + }); + connect(systemTray_, + &SystemTray::acceptPendingActivated, + [this](const QString& accountId, const QString& peerUri) { + auto& convInfo = lrcInstance_->getConversationFromPeerUri(peerUri, accountId); + if (convInfo.uid.isEmpty()) + return; + lrcInstance_->getAccountInfo(accountId).conversationModel->makePermanent( + convInfo.uid); + }); + connect(systemTray_, + &SystemTray::refusePendingActivated, + [this](const QString& accountId, const QString& peerUri) { + auto& convInfo = lrcInstance_->getConversationFromPeerUri(peerUri, accountId); + if (convInfo.uid.isEmpty()) + return; + lrcInstance_->getAccountInfo(accountId).conversationModel->removeConversation( + convInfo.uid); + }); +#endif } void @@ -60,6 +97,21 @@ ConversationsAdapter::safeInit() this, &ConversationsAdapter::onNewUnreadInteraction); + connect(&lrcInstance_->behaviorController(), + &BehaviorController::newReadInteraction, + this, + &ConversationsAdapter::onNewReadInteraction); + + connect(&lrcInstance_->behaviorController(), + &BehaviorController::newTrustRequest, + this, + &ConversationsAdapter::onNewTrustRequest); + + connect(&lrcInstance_->behaviorController(), + &BehaviorController::trustRequestTreated, + this, + &ConversationsAdapter::onTrustRequestTreated); + connect(lrcInstance_, &LRCInstance::currentAccountChanged, this, @@ -81,42 +133,7 @@ ConversationsAdapter::backToWelcomePage() void ConversationsAdapter::selectConversation(const QString& accountId, const QString& convUid) { - const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId); - - if (lrcInstance_->getCurrentConvUid() != convInfo.uid && convInfo.participants.size() > 0) { - // If the account is not currently selected, do that first, then - // proceed to select the conversation. - auto selectConversation = [this, accountId, convUid = convInfo.uid] { - const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId); - if (convInfo.uid.isEmpty()) { - return; - } - auto& accInfo = lrcInstance_->getAccountInfo(convInfo.accountId); - lrcInstance_->setSelectedConvId(convInfo.uid); - accInfo.conversationModel->clearUnreadInteractions(convInfo.uid); - - try { - // Set contact filter (for conversation tab selection) - auto& contact = accInfo.contactModel->getContact(convInfo.participants.front()); - setProperty("currentTypeFilter", QVariant::fromValue(contact.profileInfo.type)); - } catch (const std::out_of_range& e) { - qDebug() << e.what(); - } - }; - if (convInfo.accountId != lrcInstance_->getCurrAccId()) { - Utils::oneShotConnect(lrcInstance_, - &LRCInstance::currentAccountChanged, - [selectConversation] { selectConversation(); }); - lrcInstance_->setSelectedConvId(); - lrcInstance_->setSelectedAccountId(convInfo.accountId); - } else { - selectConversation(); - } - } - - if (!convInfo.uid.isEmpty()) { - Q_EMIT showConversation(lrcInstance_->getCurrAccId(), convInfo.uid); - } + lrcInstance_->selectConversation(accountId, convUid); } void @@ -151,12 +168,20 @@ ConversationsAdapter::onNewUnreadInteraction(const QString& accountId, uint64_t interactionId, const interaction::Info& interaction) { - Q_UNUSED(interactionId) if (!interaction.authorUri.isEmpty() && (!QApplication::focusWindow() || accountId != lrcInstance_->getCurrAccId() || convUid != lrcInstance_->getCurrentConvUid())) { auto& accInfo = lrcInstance_->getAccountInfo(accountId); auto from = accInfo.contactModel->bestNameForContact(interaction.authorUri); +#ifdef Q_OS_LINUX + auto notifId = QString("%1;%2;%3").arg(accountId).arg(convUid).arg(interactionId); + systemTray_->showNotification(notifId, + tr("New message"), + from + ": " + interaction.body, + NotificationType::CHAT); + +#else + Q_UNUSED(interactionId) auto onClicked = [this, accountId, convUid, uri = interaction.authorUri] { Q_EMIT lrcInstance_->notificationClicked(); const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId); @@ -166,12 +191,49 @@ ConversationsAdapter::onNewUnreadInteraction(const QString& accountId, Q_EMIT modelSorted(convInfo.uid); } }; - systemTray_->showNotification(interaction.body, from, onClicked); - return; +#endif } } +void +ConversationsAdapter::onNewReadInteraction(const QString& accountId, + const QString& convUid, + uint64_t interactionId) +{ +#ifdef Q_OS_LINUX + // hide notification + auto notifId = QString("%1;%2;%3").arg(accountId).arg(convUid).arg(interactionId); + systemTray_->hideNotification(notifId); +#endif +} + +void +ConversationsAdapter::onNewTrustRequest(const QString& accountId, const QString& peerUri) +{ +#ifdef Q_OS_LINUX + if (!QApplication::focusWindow() || accountId != lrcInstance_->getCurrAccId()) { + auto& accInfo = lrcInstance_->getAccountInfo(accountId); + auto from = accInfo.contactModel->bestNameForContact(peerUri); + auto notifId = QString("%1;%2").arg(accountId).arg(peerUri); + systemTray_->showNotification(notifId, + tr("Trust request"), + "New request from " + from, + NotificationType::REQUEST); + } +#endif +} + +void +ConversationsAdapter::onTrustRequestTreated(const QString& accountId, const QString& peerUri) +{ +#ifdef Q_OS_LINUX + // hide notification + auto notifId = QString("%1;%2").arg(accountId).arg(peerUri); + systemTray_->hideNotification(notifId); +#endif +} + void ConversationsAdapter::updateConversationsFilterWidget() { diff --git a/src/conversationsadapter.h b/src/conversationsadapter.h index cb63cd06b..8a995816e 100644 --- a/src/conversationsadapter.h +++ b/src/conversationsadapter.h @@ -68,6 +68,11 @@ private Q_SLOTS: const QString& convUid, uint64_t interactionId, const interaction::Info& interaction); + void onNewReadInteraction(const QString& accountId, + const QString& convUid, + uint64_t interactionId); + void onNewTrustRequest(const QString& accountId, const QString& peerUri); + void onTrustRequestTreated(const QString& accountId, const QString& peerUri); private: void backToWelcomePage(); diff --git a/src/lrcinstance.h b/src/lrcinstance.h index a89cd77e1..e5d8ccdc3 100644 --- a/src/lrcinstance.h +++ b/src/lrcinstance.h @@ -26,6 +26,7 @@ #include "updatemanager.h" #include "rendermanager.h" +#include "qtutils.h" #include "utils.h" #include "api/account.h" @@ -393,6 +394,43 @@ public: return callId; } + void selectConversation(const QString& accountId, const QString& convUid) + { + const auto& convInfo = getConversationFromConvUid(convUid, accountId); + + if (getCurrentConvUid() != convInfo.uid || convInfo.participants.size() > 0) { + // If the account is not currently selected, do that first, then + // proceed to select the conversation. + auto selectConversation = [this, accountId, convUid = convInfo.uid] { + const auto& convInfo = getConversationFromConvUid(convUid, accountId); + if (convInfo.uid.isEmpty()) { + return; + } + auto& accInfo = getAccountInfo(convInfo.accountId); + setSelectedConvId(convInfo.uid); + accInfo.conversationModel->clearUnreadInteractions(convInfo.uid); + + try { + // Set contact filter (for conversation tab selection) + auto& contact = accInfo.contactModel->getContact(convInfo.participants.front()); + setProperty("currentTypeFilter", QVariant::fromValue(contact.profileInfo.type)); + } catch (const std::out_of_range& e) { + qDebug() << e.what(); + } + }; + if (convInfo.accountId != getCurrAccId()) { + Utils::oneShotConnect(this, + &LRCInstance::currentAccountChanged, + [selectConversation] { selectConversation(); }); + setSelectedConvId(); + setSelectedAccountId(convInfo.accountId); + } else { + selectConversation(); + } + } + Q_EMIT conversationSelected(); + } + Q_SIGNALS: void accountListChanged(); void currentAccountChanged(); @@ -400,6 +438,7 @@ Q_SIGNALS: void notificationClicked(); void updateSmartList(); void quitEngineRequested(); + void conversationSelected(); private: std::unique_ptr<Lrc> lrc_; diff --git a/src/systemtray.cpp b/src/systemtray.cpp index c64c3c88c..33753d520 100644 --- a/src/systemtray.cpp +++ b/src/systemtray.cpp @@ -20,25 +20,215 @@ #include "appsettingsmanager.h" +#ifdef USE_LIBNOTIFY +#include <libnotify/notification.h> +#include <libnotify/notify.h> +#include <memory> + +struct Notification +{ + std::shared_ptr<NotifyNotification> nn; + QString id; +}; + +static void +openConversation(NotifyNotification*, char* action, SystemTray* nm) +{ + QStringList sl = QString(action).split(";"); + Q_EMIT nm->openConversationActivated(sl.at(1), sl.at(2)); +} + +static void +acceptPending(NotifyNotification*, char* action, SystemTray* nm) +{ + QStringList sl = QString(action).split(";"); + Q_EMIT nm->acceptPendingActivated(sl.at(1), sl.at(2)); +} + +static void +refusePending(NotifyNotification*, char* action, SystemTray* nm) +{ + QStringList sl = QString(action).split(";"); + Q_EMIT nm->refusePendingActivated(sl.at(1), sl.at(2)); +} + +void +answerCall(NotifyNotification*, char* action, SystemTray* nm) +{ + QStringList sl = QString(action).split(";"); + Q_EMIT nm->answerCallActivated(sl.at(1), sl.at(2)); +} + +void +declineCall(NotifyNotification*, char* action, SystemTray* nm) +{ + QStringList sl = QString(action).split(";"); + Q_EMIT nm->declineCallActivated(sl.at(1), sl.at(2)); +} +#endif // USE_LIBNOTIFY + +struct SystemTray::SystemTrayImpl +{ + SystemTray* parent; + SystemTrayImpl(SystemTray* parent) + : parent(parent) + {} + +#ifdef USE_LIBNOTIFY + std::map<QString, Notification> notifications; + bool actions {false}; + bool append {false}; + + void addNotificationAction(Notification& n, const QString& actionName, void* callback) + { + notify_notification_add_action(n.nn.get(), + (actionName + ";" + n.id).toStdString().c_str(), + actionName.toStdString().c_str(), + (NotifyActionCallback) callback, + this->parent, + nullptr); + } +#endif +}; + SystemTray::SystemTray(AppSettingsManager* settingsManager, QObject* parent) : QSystemTrayIcon(parent) , settingsManager_(settingsManager) -{} + , pimpl_(std::make_unique<SystemTrayImpl>(this)) +{ +#ifdef USE_LIBNOTIFY + notify_init("Jami"); + + // get notify server info + char* name = nullptr; + char* vendor = nullptr; + char* version = nullptr; + char* spec = nullptr; + + if (notify_get_server_info(&name, &vendor, &version, &spec)) { + qDebug() << QString("notify server name: %1, vendor: %2, version: %3, spec: %4") + .arg(name) + .arg(vendor) + .arg(version) + .arg(spec); + } + + // check notify server capabilities + auto serverCaps = notify_get_server_caps(); + while (serverCaps) { + if (g_strcmp0((const char*) serverCaps->data, "append") == 0 + || g_strcmp0((const char*) serverCaps->data, "x-canonical-append") == 0) { + pimpl_->append = true; + } + if (g_strcmp0((const char*) serverCaps->data, "actions") == 0) { + pimpl_->actions = true; + } + serverCaps = g_list_next(serverCaps); + } + g_list_free_full(serverCaps, g_free); +#endif +} SystemTray::~SystemTray() { +#ifdef USE_LIBNOTIFY + notify_uninit(); +#endif // USE_LIBNOTIFY hide(); } +#ifdef Q_OS_LINUX +bool +SystemTray::hideNotification(const QString& id) +{ +#if USE_LIBNOTIFY + // Search + auto notification = pimpl_->notifications.find(id); + if (notification == pimpl_->notifications.end()) { + return false; + } + + // Close + GError* error = nullptr; + if (!notify_notification_close(notification->second.nn.get(), &error)) { + qWarning("could not close notification: %s", error->message); + g_clear_error(&error); + return false; + } + + // Erase + pimpl_->notifications.erase(id); +#endif + + return true; +} + +void +SystemTray::showNotification(const QString& id, + const QString& title, + const QString& body, + NotificationType type) +{ + if (!settingsManager_->getValue(Settings::Key::EnableNotifications).toBool()) + return; + +#ifdef USE_LIBNOTIFY + // clear out an existing notification + if (pimpl_->notifications.find(id) != pimpl_->notifications.end()) + hideNotification(id); + + std::shared_ptr<NotifyNotification> notification( + notify_notification_new(title.toStdString().c_str(), body.toStdString().c_str(), nullptr), + g_object_unref); + Notification n = {notification, id}; + + pimpl_->notifications.emplace(id, n); + + // TODO: notify_notification_set_image_from_pixbuf <- GdkPixbuf + + if (type != NotificationType::CHAT) { + notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL); + notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT); + } else { + notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_NORMAL); + } + + if (pimpl_->actions) { + if (type == NotificationType::CALL) { + pimpl_->addNotificationAction(n, tr("Answer"), (void*) answerCall); + pimpl_->addNotificationAction(n, tr("Decline"), (void*) declineCall); + } else { + pimpl_->addNotificationAction(n, tr("Open conversation"), (void*) openConversation); + if (type != NotificationType::CHAT) { + pimpl_->addNotificationAction(n, tr("Accept"), (void*) acceptPending); + pimpl_->addNotificationAction(n, tr("Refuse"), (void*) refusePending); + } + } + } + + GError* error = nullptr; + notify_notification_show(notification.get(), &error); + if (error) { + qWarning("failed to show notification: %s", error->message); + g_clear_error(&error); + } +#else + Q_UNUSED(id) + Q_UNUSED(title) + Q_UNUSED(body) + Q_UNUSED(type) + Q_UNUSED(convUid) +#endif // USE_LIBNOTIFY +} + +#else void SystemTray::showNotification(const QString& message, const QString& from, std::function<void()> const& onClickedCb) { - if (!settingsManager_->getValue(Settings::Key::EnableNotifications).toBool()) { - qWarning() << "Notifications are disabled"; + if (!settingsManager_->getValue(Settings::Key::EnableNotifications).toBool()) return; - } setOnClickedCallback(std::move(onClickedCb)); @@ -55,3 +245,4 @@ SystemTray::setOnClickedCallback(Func&& onClicked) disconnect(messageClicked_); messageClicked_ = connect(this, &QSystemTrayIcon::messageClicked, onClicked); } +#endif diff --git a/src/systemtray.h b/src/systemtray.h index 8b9078a52..b648b53b5 100644 --- a/src/systemtray.h +++ b/src/systemtray.h @@ -22,6 +22,11 @@ #include <functional> +#ifdef Q_OS_LINUX +enum class NotificationType { INVALID, CALL, REQUEST, CHAT }; +Q_ENUMS(NotificationType) +#endif // Q_OS_LINUX + class AppSettingsManager; class SystemTray final : public QSystemTrayIcon @@ -32,14 +37,32 @@ public: explicit SystemTray(AppSettingsManager* settingsManager, QObject* parent = nullptr); ~SystemTray(); +#ifdef Q_OS_LINUX + bool hideNotification(const QString& id); + void showNotification(const QString& id, + const QString& title, + const QString& body, + NotificationType type); + +Q_SIGNALS: + void openConversationActivated(const QString& accountId, const QString& convUid); + void acceptPendingActivated(const QString& accountId, const QString& peerUri); + void refusePendingActivated(const QString& accountId, const QString&); + void answerCallActivated(const QString& accountId, const QString&); + void declineCallActivated(const QString& accountId, const QString&); +#else void showNotification(const QString& message, const QString& from, std::function<void()> const& onClickedCb); template<typename Func> void setOnClickedCallback(Func&& onClickedCb); +#endif // Q_OS_LINUX private: QMetaObject::Connection messageClicked_; AppSettingsManager* settingsManager_; + + struct SystemTrayImpl; + std::unique_ptr<SystemTrayImpl> pimpl_; }; -- GitLab