From cc4e824b1be9251545b8fe607015ab3bd667d8eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Thu, 12 Jul 2018 15:28:38 -0400 Subject: [PATCH] ringnotify: rewrite notification system Ring will now use three types of notifications: 1. Call notifications: opened when an incoming call is here and closed at the end of the call. 2. Request notifications: opened when a new trust request arrives and closed when the user accepts/refuse/block or just open the conversation. 3. Chat notifications: arrives with new interactions and if the current conversation is different or the client not focused. Closed by Gnome or when the conversation is opened. Change-Id: I5e5abf20507bac8bb37c429bc929c671fe66bd6b Gitlab: #868 Reviewed-by: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> --- data/cx.ring.RingGnome.gschema.xml | 4 + src/generalsettingsview.cpp | 5 + src/ring_client.cpp | 28 -- src/ringmainwindow.cpp | 456 ++++++++++++++++++++-- src/ringnotify.cpp | 592 +++++++++++++---------------- src/ringnotify.h | 42 +- ui/generalsettingsview.ui | 8 + 7 files changed, 748 insertions(+), 387 deletions(-) diff --git a/data/cx.ring.RingGnome.gschema.xml b/data/cx.ring.RingGnome.gschema.xml index 9dd2a43b..a88dbcb1 100644 --- a/data/cx.ring.RingGnome.gschema.xml +++ b/data/cx.ring.RingGnome.gschema.xml @@ -34,6 +34,10 @@ <default>true</default> <summary>Enable notifications for incoming calls.</summary> </key> + <key name="enable-pending-notifications" type="b"> + <default>true</default> + <summary>Enable notifications for pending requests.</summary> + </key> <key name="enable-chat-notifications" type="b"> <default>true</default> <summary>Enable notifications for new chat messages.</summary> diff --git a/src/generalsettingsview.cpp b/src/generalsettingsview.cpp index 8aee5a99..49945248 100644 --- a/src/generalsettingsview.cpp +++ b/src/generalsettingsview.cpp @@ -59,6 +59,7 @@ struct _GeneralSettingsViewPrivate GtkWidget *checkbutton_showstatusicon; GtkWidget *checkbutton_bringtofront; GtkWidget *checkbutton_callnotifications; + GtkWidget *checkbutton_pendingnotifications; GtkWidget *checkbutton_chatnotifications; GtkWidget *checkbutton_chatdisplaylinks; GtkWidget *checkbutton_searchentryplacescall; @@ -223,6 +224,9 @@ general_settings_view_init(GeneralSettingsView *self) g_settings_bind(priv->settings, "enable-display-links", priv->checkbutton_chatdisplaylinks, "active", G_SETTINGS_BIND_DEFAULT); + g_settings_bind(priv->settings, "enable-pending-notifications", + priv->checkbutton_pendingnotifications, "active", + G_SETTINGS_BIND_DEFAULT); g_settings_bind(priv->settings, "enable-chat-notifications", priv->checkbutton_chatnotifications, "active", G_SETTINGS_BIND_DEFAULT); @@ -316,6 +320,7 @@ general_settings_view_class_init(GeneralSettingsViewClass *klass) gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), GeneralSettingsView, checkbutton_bringtofront); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), GeneralSettingsView, checkbutton_callnotifications); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), GeneralSettingsView, checkbutton_chatdisplaylinks); + gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), GeneralSettingsView, checkbutton_pendingnotifications); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), GeneralSettingsView, checkbutton_chatnotifications); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), GeneralSettingsView, checkbutton_searchentryplacescall); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), GeneralSettingsView, radiobutton_chatright); diff --git a/src/ring_client.cpp b/src/ring_client.cpp index da36280a..d2c25ef1 100644 --- a/src/ring_client.cpp +++ b/src/ring_client.cpp @@ -119,9 +119,6 @@ struct _RingClientPrivate { NMClient *nm_client; NMActiveConnection *primary_connection; #endif - - /* notifications */ - QMetaObject::Connection call_notification; }; /* this union is used to pass ints as pointers and vice versa for GAction parameters*/ @@ -457,22 +454,6 @@ nm_client_cb(G_GNUC_UNUSED GObject *source_object, GAsyncResult *result, RingCli #endif /* USE_LIBNM */ -static void -call_notifications_toggled(RingClient *self) -{ - auto priv = RING_CLIENT_GET_PRIVATE(self); - - if (g_settings_get_boolean(priv->settings, "enable-call-notifications")) { - priv->call_notification = QObject::connect( - &CallModel::instance(), - &CallModel::incomingCall, - [] (Call *call) { ring_notify_incoming_call(call); } - ); - } else { - QObject::disconnect(priv->call_notification); - } -} - static void ring_client_startup(GApplication *app) { @@ -590,12 +571,6 @@ ring_client_startup(GApplication *app) ring_window_show(client); } ); - - /* enable notifications based on settings */ - ring_notify_init(); - call_notifications_toggled(client); - g_signal_connect_swapped(priv->settings, "changed::enable-call-notifications", G_CALLBACK(call_notifications_toggled), client); - #if USE_LIBNM /* monitor the network using libnm to notify the daemon about connectivity chagnes */ nm_client_new_async(priv->cancellable, (GAsyncReadyCallback)nm_client_cb, client); @@ -617,7 +592,6 @@ ring_client_shutdown(GApplication *app) g_object_unref(priv->cancellable); QObject::disconnect(priv->uam_updated); - QObject::disconnect(priv->call_notification); /* free the QCoreApplication, which will destroy all libRingClient models * and thus send the Unregister signal over dbus to dring */ @@ -631,8 +605,6 @@ ring_client_shutdown(GApplication *app) g_clear_object(&priv->settings); - ring_notify_uninit(); - #if USE_LIBNM /* clear NetworkManager client if it was used */ g_clear_object(&priv->nm_client); diff --git a/src/ringmainwindow.cpp b/src/ringmainwindow.cpp index c476dded..24eef1c0 100644 --- a/src/ringmainwindow.cpp +++ b/src/ringmainwindow.cpp @@ -23,7 +23,8 @@ // GTK+ related #include <glib/gi18n.h> -#include <iostream> +// std +#include <algorithm> // LRC #include <accountmodel.h> // Old lrc but still used @@ -62,6 +63,7 @@ #include "ringnotify.h" #include "accountinfopointer.h" #include "native/pixbufmanipulator.h" +#include "ringnotify.h" //============================================================================== @@ -111,11 +113,18 @@ struct RingMainWindowPrivate GtkWidget *scrolled_window_contact_requests; GtkWidget *webkit_chat_container; ///< The webkit_chat_container is created once, then reused for all chat views + GtkWidget *notifier; + GSettings *settings; details::CppImpl* cpp; ///< Non-UI and C++ only code gulong update_download_folder; + gulong notif_chat_view; + gulong notif_accept_pending; + gulong notif_refuse_pending; + gulong notif_accept_call; + gulong notif_decline_call; }; G_DEFINE_TYPE_WITH_PRIVATE(RingMainWindow, ring_main_window, GTK_TYPE_APPLICATION_WINDOW); @@ -304,17 +313,22 @@ public: QMetaObject::Connection showChatViewConnection_; QMetaObject::Connection showCallViewConnection_; QMetaObject::Connection showIncomingViewConnection_; + QMetaObject::Connection newTrustRequestNotification_; + QMetaObject::Connection closeTrustRequestNotification_; + QMetaObject::Connection slotNewInteraction_; + QMetaObject::Connection slotReadInteraction_; QMetaObject::Connection changeAccountConnection_; QMetaObject::Connection newAccountConnection_; QMetaObject::Connection rmAccountConnection_; QMetaObject::Connection invalidAccountConnection_; QMetaObject::Connection historyClearedConnection_; QMetaObject::Connection modelSortedConnection_; + QMetaObject::Connection callChangedConnection_; + QMetaObject::Connection newIncomingCallConnection_; QMetaObject::Connection filterChangedConnection_; QMetaObject::Connection newConversationConnection_; QMetaObject::Connection conversationRemovedConnection_; QMetaObject::Connection accountStatusChangedConnection_; - QMetaObject::Connection chatNotification_; private: CppImpl() = delete; @@ -325,7 +339,6 @@ private: GtkWidget* displayIncomingView(lrc::api::conversation::Info); GtkWidget* displayCurrentCallView(lrc::api::conversation::Info); GtkWidget* displayChatView(lrc::api::conversation::Info); - void chatNotifications(); // Callbacks used as LRC Qt slot void slotAccountAddedFromLrc(const std::string& id); @@ -334,12 +347,19 @@ private: void slotAccountStatusChanged(const std::string& id); void slotConversationCleared(const std::string& uid); void slotModelSorted(); + void slotNewIncomingCall(const std::string& callId); + void slotCallStatusChanged(const std::string& callId); void slotFilterChanged(); void slotNewConversation(const std::string& uid); void slotConversationRemoved(const std::string& uid); void slotShowChatView(const std::string& id, lrc::api::conversation::Info origin); void slotShowCallView(const std::string& id, lrc::api::conversation::Info origin); void slotShowIncomingCallView(const std::string& id, lrc::api::conversation::Info origin); + void slotNewTrustRequest(const std::string& id, const std::string& contactUri); + void slotCloseTrustRequest(const std::string& id, const std::string& contactUri); + void slotNewInteraction(const std::string& accountId, const std::string& conversation, + uint64_t, const lrc::api::interaction::Info& interaction); + void slotCloseInteraction(const std::string& accountId, const std::string& conversation, uint64_t); }; inline namespace gtk_callbacks @@ -646,7 +666,6 @@ on_handle_account_migrations(RingMainWindow* self) for (const auto& accountId : accounts) { priv->cpp->accountInfoForMigration_ = &priv->cpp->lrc_->getAccountModel().getAccountInfo(accountId); if (priv->cpp->accountInfoForMigration_->status == lrc::api::account::Status::ERROR_NEED_MIGRATION) { - std::cout << "DO MIGRATION" << std::endl; priv->account_migration_view = account_migration_view_new(priv->cpp->accountInfoForMigration_); g_signal_connect_swapped(priv->account_migration_view, "account-migration-completed", G_CALLBACK(on_handle_account_migrations), self); @@ -678,6 +697,155 @@ on_handle_account_migrations(RingMainWindow* self) priv->cpp->showAccountSelectorWidget(); } +enum class Action { + SELECT, + ACCEPT, + REFUSE +}; + +static void +action_notification(gchar* title, RingMainWindow* self, Action action) +{ + g_return_if_fail(IS_RING_MAIN_WINDOW(self) && title); + auto* priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(self)); + if (!priv->cpp->accountInfo_) { + g_warning("Notification clicked but accountInfo_ currently empty"); + return; + } + + std::string titleStr = title; + auto firstMarker = titleStr.find(":"); + if (firstMarker == std::string::npos) return; + auto secondMarker = titleStr.find(":", firstMarker + 1); + if (secondMarker == std::string::npos) return; + + auto id = titleStr.substr(0, firstMarker); + auto type = titleStr.substr(firstMarker + 1, secondMarker - firstMarker - 1); + auto information = titleStr.substr(secondMarker + 1); + + if (action == Action::SELECT) { + // Select conversation + if (priv->cpp->show_settings) { + priv->cpp->leaveSettingsView(); + } + + if (priv->cpp->accountInfo_->id != id) { + priv->cpp->updateLrc(id); + } + + if (type == "interaction") { + priv->cpp->accountInfo_->conversationModel->selectConversation(information); + conversations_view_select_conversation(CONVERSATIONS_VIEW(priv->treeview_conversations), information); + } else if (type == "request") { + for (const auto& conversation : priv->cpp->accountInfo_->conversationModel->getFilteredConversations(lrc::api::profile::Type::PENDING)) { + auto current_page = gtk_notebook_get_current_page(GTK_NOTEBOOK(priv->notebook_contacts)); + auto contactRequestsPageNum = gtk_notebook_page_num(GTK_NOTEBOOK(priv->notebook_contacts), + priv->scrolled_window_contact_requests); + gtk_notebook_set_current_page(GTK_NOTEBOOK(priv->notebook_contacts), contactRequestsPageNum); + if (!conversation.participants.empty() && conversation.participants.front() == information) { + priv->cpp->accountInfo_->conversationModel->selectConversation(conversation.uid); + } + conversations_view_select_conversation(CONVERSATIONS_VIEW(priv->treeview_conversations), conversation.uid); + } + } + } else { + // accept or refuse notifiation + try { + auto& accountInfo = priv->cpp->lrc_->getAccountModel().getAccountInfo(id); + for (const auto& conversation : accountInfo.conversationModel->getFilteredConversations(lrc::api::profile::Type::PENDING)) { + if (!conversation.participants.empty() && conversation.participants.front() == information) { + if (action == Action::ACCEPT) { + accountInfo.conversationModel->makePermanent(conversation.uid); + } else { + accountInfo.conversationModel->removeConversation(conversation.uid); + } + } + } + } catch (const std::out_of_range& e) { + g_warning("Can't get account %i: %s", id.c_str(), e.what()); + } + } + +} + +static void +on_notification_chat_clicked(GtkWidget* notifier, gchar *title, RingMainWindow* self) +{ + action_notification(title, self, Action::SELECT); +} + +static void +on_notification_accept_pending(GtkWidget*, gchar *title, RingMainWindow* self) +{ + action_notification(title, self, Action::ACCEPT); +} + +static void +on_notification_refuse_pending(GtkWidget*, gchar *title, RingMainWindow* self) +{ + action_notification(title, self, Action::REFUSE); +} + +static void +on_notification_accept_call(GtkWidget* notifier, gchar *title, RingMainWindow* self) +{ + g_return_if_fail(IS_RING_MAIN_WINDOW(self) && title); + auto* priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(self)); + if (!priv->cpp->accountInfo_) { + g_warning("Notification clicked but accountInfo_ currently empty"); + return; + } + + std::string titleStr = title; + auto firstMarker = titleStr.find(":"); + if (firstMarker == std::string::npos) return; + auto secondMarker = titleStr.find(":", firstMarker + 1); + if (secondMarker == std::string::npos) return; + + auto id = titleStr.substr(0, firstMarker); + auto type = titleStr.substr(firstMarker + 1, secondMarker - firstMarker - 1); + auto information = titleStr.substr(secondMarker + 1); + + if (priv->cpp->show_settings) { + priv->cpp->leaveSettingsView(); + } + + try { + auto& accountInfo = priv->cpp->lrc_->getAccountModel().getAccountInfo(id); + accountInfo.callModel->accept(information); + } catch (const std::out_of_range& e) { + g_warning("Can't get account %i: %s", id.c_str(), e.what()); + } +} + +static void +on_notification_decline_call(GtkWidget* notifier, gchar *title, RingMainWindow* self) +{ + g_return_if_fail(IS_RING_MAIN_WINDOW(self) && title); + auto* priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(self)); + if (!priv->cpp->accountInfo_) { + g_warning("Notification clicked but accountInfo_ currently empty"); + return; + } + + std::string titleStr = title; + auto firstMarker = titleStr.find(":"); + if (firstMarker == std::string::npos) return; + auto secondMarker = titleStr.find(":", firstMarker + 1); + if (secondMarker == std::string::npos) return; + + auto id = titleStr.substr(0, firstMarker); + auto type = titleStr.substr(firstMarker + 1, secondMarker - firstMarker - 1); + auto information = titleStr.substr(secondMarker + 1); + + try { + auto& accountInfo = priv->cpp->lrc_->getAccountModel().getAccountInfo(id); + accountInfo.callModel->hangUp(information); + } catch (const std::out_of_range& e) { + g_warning("Can't get account %i: %s", id.c_str(), e.what()); + } +} + } // namespace gtk_callbacks CppImpl::CppImpl(RingMainWindow& widget) @@ -686,23 +854,6 @@ CppImpl::CppImpl(RingMainWindow& widget) , lrc_ {std::make_unique<lrc::api::Lrc>()} {} -void -CppImpl::chatNotifications() -{ - chatNotification_ = QObject::connect( - &Media::RecordingModel::instance(), - &Media::RecordingModel::newTextMessage, - [this] (Media::TextRecording* t, ContactMethod* cm) { - if ((chatViewConversation_ - && chatViewConversation_->participants[0] == cm->uri().toStdString()) - || not g_settings_get_boolean(widgets->settings, "enable-chat-notifications")) - return; - - ring_notify_message(cm, t); - } - ); -} - static gboolean on_clear_all_history_foreach(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer self) { @@ -957,10 +1108,6 @@ CppImpl::init() } } } - - // setup chat notification - chatNotifications(); - // delete obsolete history if (not accountIds.empty()) { auto days = g_settings_get_int(widgets->settings, "history-limit"); @@ -975,6 +1122,17 @@ CppImpl::init() if (accounts.empty()) { enterAccountCreationWizard(); } + + widgets->notif_chat_view = g_signal_connect(widgets->notifier, "showChatView", + G_CALLBACK(on_notification_chat_clicked), self); + widgets->notif_accept_pending = g_signal_connect(widgets->notifier, "acceptPending", + G_CALLBACK(on_notification_accept_pending), self); + widgets->notif_refuse_pending = g_signal_connect(widgets->notifier, "refusePending", + G_CALLBACK(on_notification_refuse_pending), self); + widgets->notif_accept_call = g_signal_connect(widgets->notifier, "acceptCall", + G_CALLBACK(on_notification_accept_call), self); + widgets->notif_decline_call = g_signal_connect(widgets->notifier, "declineCall", + G_CALLBACK(on_notification_decline_call), self); } CppImpl::~CppImpl() @@ -983,6 +1141,8 @@ CppImpl::~CppImpl() QObject::disconnect(showIncomingViewConnection_); QObject::disconnect(historyClearedConnection_); QObject::disconnect(modelSortedConnection_); + QObject::disconnect(callChangedConnection_); + QObject::disconnect(newIncomingCallConnection_); QObject::disconnect(filterChangedConnection_); QObject::disconnect(newConversationConnection_); QObject::disconnect(conversationRemovedConnection_); @@ -991,9 +1151,11 @@ CppImpl::~CppImpl() QObject::disconnect(rmAccountConnection_); QObject::disconnect(invalidAccountConnection_); QObject::disconnect(showCallViewConnection_); - QObject::disconnect(modelSortedConnection_); + QObject::disconnect(newTrustRequestNotification_); + QObject::disconnect(closeTrustRequestNotification_); + QObject::disconnect(slotNewInteraction_); + QObject::disconnect(slotReadInteraction_); QObject::disconnect(accountStatusChangedConnection_); - QObject::disconnect(chatNotification_); g_clear_object(&widgets->welcome_view); g_clear_object(&widgets->webkit_chat_container); @@ -1070,6 +1232,16 @@ CppImpl::displayChatView(lrc::api::conversation::Info conversation) chatViewConversation_.reset(new lrc::api::conversation::Info(conversation)); auto* new_view = chat_view_new(webkitChatContainer(), accountInfo_, chatViewConversation_.get()); g_signal_connect_swapped(new_view, "hide-view-clicked", G_CALLBACK(on_hide_view_clicked), self); + try { + auto contactUri = chatViewConversation_->participants.front(); + auto contactInfo = accountInfo_->contactModel->getContact(contactUri); + auto isPending = contactInfo.profileInfo.type == lrc::api::profile::Type::PENDING; + if (isPending) { + auto notifId = accountInfo_->id + ":request:" + contactUri; + ring_hide_notification(RING_NOTIFIER(widgets->notifier), notifId); + } + } catch(...) { } + return new_view; } @@ -1369,7 +1541,13 @@ CppImpl::updateLrc(const std::string& id, const std::string& accountIdToFlagFree QObject::disconnect(showIncomingViewConnection_); QObject::disconnect(changeAccountConnection_); QObject::disconnect(showCallViewConnection_); + QObject::disconnect(newTrustRequestNotification_); + QObject::disconnect(closeTrustRequestNotification_); + QObject::disconnect(slotNewInteraction_); + QObject::disconnect(slotReadInteraction_); QObject::disconnect(modelSortedConnection_); + QObject::disconnect(callChangedConnection_); + QObject::disconnect(newIncomingCallConnection_); QObject::disconnect(historyClearedConnection_); QObject::disconnect(filterChangedConnection_); QObject::disconnect(newConversationConnection_); @@ -1421,6 +1599,14 @@ CppImpl::updateLrc(const std::string& id, const std::string& accountIdToFlagFree &lrc::api::ConversationModel::modelSorted, [this] { slotModelSorted(); }); + callChangedConnection_ = QObject::connect(&*accountInfo_->callModel, + &lrc::api::NewCallModel::callStatusChanged, + [this] (const std::string& callId) { slotCallStatusChanged(callId); }); + + newIncomingCallConnection_ = QObject::connect(&*accountInfo_->callModel, + &lrc::api::NewCallModel::newIncomingCall, + [this] (const std::string&, const std::string& callId) { slotNewIncomingCall(callId); }); + filterChangedConnection_ = QObject::connect(&*accountInfo_->conversationModel, &lrc::api::ConversationModel::filterChanged, [this] { slotFilterChanged(); }); @@ -1441,9 +1627,29 @@ CppImpl::updateLrc(const std::string& id, const std::string& accountIdToFlagFree &lrc::api::BehaviorController::showCallView, [this] (const std::string& id, lrc::api::conversation::Info origin) { slotShowCallView(id, origin); }); + newTrustRequestNotification_ = QObject::connect(&lrc_->getBehaviorController(), + &lrc::api::BehaviorController::newTrustRequest, + [this] (const std::string& id, const std::string& contactUri) { slotNewTrustRequest(id, contactUri); }); + + closeTrustRequestNotification_ = QObject::connect(&lrc_->getBehaviorController(), + &lrc::api::BehaviorController::trustRequestTreated, + [this] (const std::string& id, const std::string& contactUri) { slotCloseTrustRequest(id, contactUri); }); + showIncomingViewConnection_ = QObject::connect(&lrc_->getBehaviorController(), &lrc::api::BehaviorController::showIncomingCallView, - [this] (const std::string& id, lrc::api::conversation::Info origin) { slotShowIncomingCallView(id, origin); }); + [this] (const std::string& id, lrc::api::conversation::Info origin) + { slotShowIncomingCallView(id, origin); }); + + slotNewInteraction_ = QObject::connect(&lrc_->getBehaviorController(), + &lrc::api::BehaviorController::newUnreadInteraction, + [this] (const std::string& accountId, const std::string& conversation, + uint64_t interactionId, const lrc::api::interaction::Info& interaction) + { slotNewInteraction(accountId, conversation, interactionId, interaction); }); + + slotReadInteraction_ = QObject::connect(&lrc_->getBehaviorController(), + &lrc::api::BehaviorController::newReadInteraction, + [this] (const std::string& accountId, const std::string& conversation, uint64_t interactionId) + { slotCloseInteraction(accountId, conversation, interactionId); }); const gchar *text = gtk_entry_get_text(GTK_ENTRY(widgets->search_entry)); currentTypeFilter_ = accountInfo_->profileInfo.type; @@ -1561,6 +1767,78 @@ CppImpl::slotModelSorted() refreshPendingContactRequestTab(); } +void +CppImpl::slotCallStatusChanged(const std::string& callId) +{ + if (!accountInfo_) { + return; + } + try { + auto call = accountInfo_->callModel->getCall(callId); + auto peer = call.peer; + if (accountInfo_->profileInfo.type == lrc::api::profile::Type::RING && peer.find("ring:") == 0) { + peer = peer.substr(5); + } + auto& contactModel = accountInfo_->contactModel; + std::string notifId = ""; + try { + notifId = accountInfo_->id + ":call:" + callId; + } catch (...) { + g_warning("Can't get contact for account %s. Don't show notification", accountInfo_->id.c_str()); + return; + } + + if (call.status == lrc::api::call::Status::IN_PROGRESS + || call.status == lrc::api::call::Status::ENDED) { + // Call ended, close the notification + ring_hide_notification(RING_NOTIFIER(widgets->notifier), notifId); + } + } catch (const std::exception& e) { + g_warning("Can't get call %lu for this account.", callId); + } +} + +void +CppImpl::slotNewIncomingCall(const std::string& callId) +{ + if (!accountInfo_) { + return; + } + try { + auto call = accountInfo_->callModel->getCall(callId); + auto peer = call.peer; + if (accountInfo_->profileInfo.type == lrc::api::profile::Type::RING && peer.find("ring:") == 0) { + peer = peer.substr(5); + } + auto& contactModel = accountInfo_->contactModel; + std::string avatar = "", name = "", notifId = "", uri = ""; + try { + auto contactInfo = contactModel->getContact(peer); + uri = contactInfo.profileInfo.uri; + avatar = contactInfo.profileInfo.avatar; + name = contactInfo.profileInfo.alias; + if (name.empty()) { + name = contactInfo.registeredName; + if (name.empty()) { + name = contactInfo.profileInfo.uri; + } + } + notifId = accountInfo_->id + ":call:" + callId; + } catch (...) { + g_warning("Can't get contact for account %s. Don't show notification", accountInfo_->id.c_str()); + return; + } + + if (g_settings_get_boolean(widgets->settings, "enable-call-notifications")) { + name.erase(std::remove(name.begin(), name.end(), '\r'), name.end()); + auto body = name + _(" is calling you!"); + ring_show_notification(RING_NOTIFIER(widgets->notifier), avatar, uri, name, notifId, _("Incoming call"), body, NotificationType::CALL); + } + } catch (const std::exception& e) { + g_warning("Can't get call %lu for this account.", callId); + } +} + void CppImpl::slotFilterChanged() { @@ -1674,10 +1952,119 @@ CppImpl::slotShowCallView(const std::string& id, lrc::api::conversation::Info or changeView(CURRENT_CALL_VIEW_TYPE, origin); } +void +CppImpl::slotNewTrustRequest(const std::string& id, const std::string& contactUri) +{ + try { + auto& accountInfo = lrc_->getAccountModel().getAccountInfo(id); + auto notifId = accountInfo.id + ":request:" + contactUri; + std::string avatar = "", name = "", uri = ""; + auto& contactModel = accountInfo.contactModel; + try { + auto contactInfo = contactModel->getContact(contactUri); + uri = contactInfo.profileInfo.uri; + avatar = contactInfo.profileInfo.avatar; + name = contactInfo.profileInfo.alias; + if (name.empty()) { + name = contactInfo.registeredName; + if (name.empty()) { + name = contactInfo.profileInfo.uri; + } + } + } catch (...) { + g_warning("Can't get contact for account %s. Don't show notification", accountInfo.id.c_str()); + return; + } + if (g_settings_get_boolean(widgets->settings, "enable-pending-notifications")) { + name.erase(std::remove(name.begin(), name.end(), '\r'), name.end()); + auto body = _("New request from ") + name; + ring_show_notification(RING_NOTIFIER(widgets->notifier), avatar, uri, name, notifId, _("Trust request"), body, NotificationType::REQUEST); + } + } catch (...) { + g_warning("Can't get account %s", id.c_str()); + } +} + +void +CppImpl::slotCloseTrustRequest(const std::string& id, const std::string& contactUri) +{ + try { + auto& accountInfo = lrc_->getAccountModel().getAccountInfo(id); + auto notifId = accountInfo.id + ":request:" + contactUri; + ring_hide_notification(RING_NOTIFIER(widgets->notifier), notifId); + } catch (...) { + g_warning("Can't get account %s", id.c_str()); + } +} + +void +CppImpl::slotNewInteraction(const std::string& accountId, const std::string& conversation, + uint64_t, const lrc::api::interaction::Info& interaction) +{ + if (chatViewConversation_ && chatViewConversation_->uid == conversation) { + if (gtk_window_is_active(GTK_WINDOW(self))) { + return; + } + } + try { + auto& accountInfo = lrc_->getAccountModel().getAccountInfo(accountId); + auto notifId = accountInfo.id + ":interaction:" + conversation; + auto& contactModel = accountInfo.contactModel; + auto& conversationModel = accountInfo.conversationModel; + for (const auto& conv : conversationModel->allFilteredConversations()) + { + if (conv.uid == conversation) { + if (conv.participants.empty()) return; + std::string avatar = "", name = "", uri = ""; + try { + auto contactInfo = contactModel->getContact(conv.participants.front()); + uri = contactInfo.profileInfo.uri; + avatar = contactInfo.profileInfo.avatar; + name = contactInfo.profileInfo.alias; + if (name.empty()) { + name = contactInfo.registeredName; + if (name.empty()) { + name = contactInfo.profileInfo.uri; + } + } + } catch (...) { + g_warning("Can't get contact for account %s. Don't show notification", accountInfo.id.c_str()); + return; + } + + if (g_settings_get_boolean(widgets->settings, "enable-chat-notifications")) { + name.erase(std::remove(name.begin(), name.end(), '\r'), name.end()); + auto body = name + ": " + interaction.body; + ring_show_notification(RING_NOTIFIER(widgets->notifier), avatar, uri, name, notifId, _("New message"), body, NotificationType::CHAT); + } + } + } + } catch (...) { + g_warning("Can't get account %s", accountId.c_str()); + } +} + +void +CppImpl::slotCloseInteraction(const std::string& accountId, const std::string& conversation, uint64_t) +{ + if (!gtk_window_is_active(GTK_WINDOW(self)) + || (chatViewConversation_ && chatViewConversation_->uid != conversation)) { + return; + } + try { + auto& accountInfo = lrc_->getAccountModel().getAccountInfo(accountId); + auto notifId = accountInfo.id + ":interaction:" + conversation; + ring_hide_notification(RING_NOTIFIER(widgets->notifier), notifId); + } catch (...) { + g_warning("Can't get account %s", accountId.c_str()); + } +} + void CppImpl::slotShowIncomingCallView(const std::string& id, lrc::api::conversation::Info origin) { changeAccountSelection(id); + // Change the view if we want a different view. auto* old_view = gtk_bin_get_child(GTK_BIN(widgets->frame_call)); @@ -1710,6 +2097,7 @@ ring_main_window_init(RingMainWindow *win) // CppImpl ctor priv->cpp = new details::CppImpl {*win}; + priv->notifier = ring_notifier_new(); priv->cpp->init(); } @@ -1721,12 +2109,24 @@ ring_main_window_dispose(GObject *object) delete priv->cpp; priv->cpp = nullptr; + delete priv->notifier; + priv->notifier = nullptr; G_OBJECT_CLASS(ring_main_window_parent_class)->dispose(object); if (priv->general_settings_view) { g_signal_handler_disconnect(priv->general_settings_view, priv->update_download_folder); priv->update_download_folder = 0; + g_signal_handler_disconnect(priv->notifier, priv->notif_chat_view); + priv->notif_chat_view = 0; + g_signal_handler_disconnect(priv->notifier, priv->notif_accept_pending); + priv->notif_accept_pending = 0; + g_signal_handler_disconnect(priv->notifier, priv->notif_refuse_pending); + priv->notif_refuse_pending = 0; + g_signal_handler_disconnect(priv->notifier, priv->notif_accept_call); + priv->notif_accept_call = 0; + g_signal_handler_disconnect(priv->notifier, priv->notif_decline_call); + priv->notif_decline_call = 0; } } diff --git a/src/ringnotify.cpp b/src/ringnotify.cpp index e36373fa..e26f3bf5 100644 --- a/src/ringnotify.cpp +++ b/src/ringnotify.cpp @@ -1,6 +1,7 @@ /* * Copyright (C) 2015-2018 Savoir-faire Linux Inc. * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> + * Author: Sebastien Blin <sebastien.blin@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,13 +41,10 @@ #include <recentmodel.h> #endif -#if USE_LIBNOTIFY -static constexpr int MAX_NOTIFICATIONS = 10; // max unread chat msgs to display from the same contact static constexpr const char* SERVER_NOTIFY_OSD = "notify-osd"; static constexpr const char* NOTIFICATION_FILE = SOUNDSDIR "/ringtone_notify.wav"; - /* struct to store the parsed list of the notify server capabilities */ struct RingNotifyServerInfo { @@ -69,22 +67,110 @@ struct RingNotifyServerInfo } }; -static struct RingNotifyServerInfo server_info; +namespace details +{ +class CppImpl; +} + +struct _RingNotifier +{ + GtkBox parent; +}; + +struct _RingNotifierClass +{ + GtkBoxClass parent_class; +}; + +typedef struct _RingNotifierPrivate RingNotifierPrivate; + +struct _RingNotifierPrivate +{ + RingNotifyServerInfo serverInfo; + + details::CppImpl* cpp; ///< Non-UI and C++ only code0 +}; + +G_DEFINE_TYPE_WITH_PRIVATE(RingNotifier, ring_notifier, GTK_TYPE_BOX); + +#define RING_NOTIFIER_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), RING_NOTIFIER_TYPE, RingNotifierPrivate)) + +/* signals */ +enum { + SHOW_CHAT, + ACCEPT_PENDING, + REFUSE_PENDING, + ACCEPT_CALL, + DECLINE_CALL, + LAST_SIGNAL +}; + +static guint ring_notifier_signals[LAST_SIGNAL] = { 0 }; + +namespace details +{ + +class CppImpl +{ +public: + explicit CppImpl(RingNotifier& widget); + ~CppImpl(); + + RingNotifier* self = nullptr; // The GTK widget itself + RingNotifierPrivate* priv = nullptr; + + std::map<std::string, std::shared_ptr<NotifyNotification>> notifications_; +private: + CppImpl() = delete; + CppImpl(const CppImpl&) = delete; + CppImpl& operator=(const CppImpl&) = delete; + +}; + +CppImpl::CppImpl(RingNotifier& widget) + : self {&widget} +{} + +CppImpl::~CppImpl() +{} + +} + +static void +ring_notifier_dispose(GObject *object) +{ + auto* self = RING_NOTIFIER(object); + auto* priv = RING_NOTIFIER_GET_PRIVATE(self); + + delete priv->cpp; + priv->cpp = nullptr; + +#if USE_LIBNOTIFY + if (notify_is_initted()) + notify_uninit(); #endif -void -ring_notify_init() + G_OBJECT_CLASS(ring_notifier_parent_class)->dispose(object); +} + +static void +ring_notifier_init(RingNotifier *view) { + gtk_widget_init_template(GTK_WIDGET(view)); + + RingNotifierPrivate *priv = RING_NOTIFIER_GET_PRIVATE(view); + priv->cpp = new details::CppImpl {*view}; + #if USE_LIBNOTIFY notify_init("Ring"); /* get notify server info */ - if (notify_get_server_info(&server_info.name, - &server_info.vendor, - &server_info.version, - &server_info.spec)) { + if (notify_get_server_info(&(priv->serverInfo).name, + &(priv->serverInfo).vendor, + &(priv->serverInfo).version, + &(priv->serverInfo).spec)) { g_debug("notify server name: %s, vendor: %s, version: %s, spec: %s", - server_info.name, server_info.vendor, server_info.version, server_info.spec); + priv->serverInfo.name, priv->serverInfo.vendor, priv->serverInfo.version, priv->serverInfo.spec); } /* check notify server capabilities */ @@ -92,10 +178,10 @@ ring_notify_init() while (list) { if (g_strcmp0((const char *)list->data, "append") == 0 || g_strcmp0((const char *)list->data, "x-canonical-append") == 0) { - server_info.append = TRUE; + priv->serverInfo.append = TRUE; } if (g_strcmp0((const char *)list->data, "actions") == 0) { - server_info.actions = TRUE; + priv->serverInfo.actions = TRUE; } list = g_list_next(list); @@ -105,359 +191,229 @@ ring_notify_init() #endif } -void -ring_notify_uninit() +static void +ring_notifier_class_init(RingNotifierClass *klass) { -#if USE_LIBNOTIFY - if (notify_is_initted()) - notify_uninit(); -#endif + G_OBJECT_CLASS(klass)->dispose = ring_notifier_dispose; + + ring_notifier_signals[SHOW_CHAT] = g_signal_new( + "showChatView", + G_TYPE_FROM_CLASS(klass), + (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED), + 0, + nullptr, + nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); + + ring_notifier_signals[ACCEPT_PENDING] = g_signal_new( + "acceptPending", + G_TYPE_FROM_CLASS(klass), + (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED), + 0, + nullptr, + nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); + + ring_notifier_signals[REFUSE_PENDING] = g_signal_new( + "refusePending", + G_TYPE_FROM_CLASS(klass), + (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED), + 0, + nullptr, + nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); + + ring_notifier_signals[ACCEPT_CALL] = g_signal_new( + "acceptCall", + G_TYPE_FROM_CLASS(klass), + (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED), + 0, + nullptr, + nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); + + ring_notifier_signals[DECLINE_CALL] = g_signal_new( + "declineCall", + G_TYPE_FROM_CLASS(klass), + (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED), + 0, + nullptr, + nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); } -gboolean -ring_notify_is_initted() +GtkWidget * +ring_notifier_new() { -#if USE_LIBNOTIFY - return notify_is_initted(); -#else - return FALSE; -#endif + gpointer view = g_object_new(RING_NOTIFIER_TYPE, NULL); + return (GtkWidget *)view; } #if USE_LIBNOTIFY static void -ring_notify_show_cm(NotifyNotification*, char *, ContactMethod *cm) +show_chat_view(NotifyNotification*, char* id, RingNotifier* view) { - /* show the main window in case its hidden */ - if (auto action = g_action_map_lookup_action(G_ACTION_MAP(g_application_get_default()), "show-main-window")) { - g_action_change_state(action, g_variant_new_boolean(TRUE)); - } - /* select the relevant cm */ - auto idx = RecentModel::instance().getIndex(cm); - if (idx.isValid()) { - RecentModel::instance().selectionModel()->setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect); - } + g_signal_emit(G_OBJECT(view), ring_notifier_signals[SHOW_CHAT], 0, id); } -#endif -gboolean -ring_notify_incoming_call( -#if !USE_LIBNOTIFY - G_GNUC_UNUSED -#endif - Call* call) +static void +accept_pending(NotifyNotification*, char* id, RingNotifier* view) { - gboolean success = FALSE; -#if USE_LIBNOTIFY - g_return_val_if_fail(call, FALSE); - - gchar *body = g_markup_escape_text(call->formattedName().toUtf8().constData(), -1); - std::shared_ptr<NotifyNotification> notification( - notify_notification_new(_("Incoming call"), body, NULL), g_object_unref); - g_free(body); - - /* get photo */ - QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto( - call->peerContactMethod(), QSize(50, 50), false); - std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>(); - notify_notification_set_image_from_pixbuf(notification.get(), photo.get()); - - /* calls have highest urgency */ - notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL); - notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT); - - /* if the notification server supports actions, make the default action to show the call */ - if (server_info.actions) { - notify_notification_add_action(notification.get(), - "default", - C_("notification action name", "Show"), - (NotifyActionCallback)ring_notify_show_cm, - call->peerContactMethod(), - nullptr); - } - - GError *error = NULL; - success = notify_notification_show(notification.get(), &error); - - if (success) { - /* monitor the life cycle of the call and try to close the notification - * once the call has been aswered */ - auto state_changed_conn = std::make_shared<QMetaObject::Connection>(); - *state_changed_conn = QObject::connect( - call, - &Call::lifeCycleStateChanged, - [notification, state_changed_conn] (Call::LifeCycleState newState, G_GNUC_UNUSED Call::LifeCycleState previousState) - { - g_return_if_fail(NOTIFY_IS_NOTIFICATION(notification.get())); - if (newState > Call::LifeCycleState::INITIALIZATION) { - /* note: not all systems will actually close the notification - * even if the above function returns as true */ - if (!notify_notification_close(notification.get(), NULL)) - g_warning("could not close notification"); - - /* once we (try to) close the notification, we can - * disconnect from this signal; this should also destroy - * the notification shared_ptr as its ref count will - * drop to 0 */ - QObject::disconnect(*state_changed_conn); - } - } - ); - } else { - g_warning("failed to show notification: %s", error->message); - g_clear_error(&error); - } -#endif - return success; + std::string newId = id; + g_signal_emit(G_OBJECT(view), ring_notifier_signals[ACCEPT_PENDING], 0, newId.substr(std::string("add:").length()).c_str()); } -#if USE_LIBNOTIFY - static void -ring_notify_free_list(gpointer, GList *value, gpointer) +refuse_pending(NotifyNotification*, char* id, RingNotifier* view) { - if (value) { - g_object_unref(G_OBJECT(value->data)); - g_list_free(value); - } + std::string newId = id; + g_signal_emit(G_OBJECT(view), ring_notifier_signals[REFUSE_PENDING], 0, newId.substr(std::string("rm:").length()).c_str()); } static void -ring_notify_free_chat_table(GHashTable *table) { - if (table) { - g_hash_table_foreach(table, (GHFunc)ring_notify_free_list, nullptr); - g_hash_table_destroy(table); - } -} - -/** - * Returns a pointer to a GHashTable which contains key,value pairs where a ContactMethod pointer - * is the key and a GList of notifications for that CM is the vlue. - */ -GHashTable * -ring_notify_get_chat_table() +accept_call(NotifyNotification*, char* id, RingNotifier* view) { - static std::unique_ptr<GHashTable, decltype(ring_notify_free_chat_table)&> chat_table( - nullptr, ring_notify_free_chat_table); - - if (chat_table.get() == nullptr) - chat_table.reset(g_hash_table_new(NULL, NULL)); - - return chat_table.get(); + std::string newId = id; + g_signal_emit(G_OBJECT(view), ring_notifier_signals[ACCEPT_CALL], 0, newId.substr(std::string("accept:").length()).c_str()); } static void -notification_closed(NotifyNotification *notification, ContactMethod *cm) +decline_call(NotifyNotification*, char* id, RingNotifier* view) { - g_return_if_fail(cm); - - /* remove from the list */ - auto chat_table = ring_notify_get_chat_table(); - if (auto list = (GList *)g_hash_table_lookup(chat_table, cm)) { - list = g_list_remove(list, notification); - if (list) { - // the head of the list may have changed - g_hash_table_replace(chat_table, cm, list); - } else { - g_hash_table_remove(chat_table, cm); - } - } - - g_object_unref(notification); + std::string newId = id; + g_signal_emit(G_OBJECT(view), ring_notifier_signals[DECLINE_CALL], 0, newId.substr(std::string("decline:").length()).c_str()); } -static gboolean -ring_notify_show_text_message(ContactMethod *cm, const QModelIndex& idx) +#endif + +gboolean +ring_show_notification(RingNotifier* view, const std::string& icon, + const std::string& uri, const std::string& name, + const std::string& id, const std::string& title, + const std::string& body, NotificationType type) { - g_return_val_if_fail(idx.isValid() && cm, FALSE); + g_return_val_if_fail(IS_RING_NOTIFIER(view), false); gboolean success = FALSE; + RingNotifierPrivate *priv = RING_NOTIFIER_GET_PRIVATE(view); - auto title = g_markup_printf_escaped(C_("Text message notification", "%s says:"), idx.data(static_cast<int>(Ring::Role::Name)).toString().toUtf8().constData()); - auto body = g_markup_escape_text(idx.data(Qt::DisplayRole).toString().toUtf8().constData(), -1); - - NotifyNotification *notification_new = nullptr; - NotifyNotification *notification_old = nullptr; - - /* try to get the previous notification */ - auto chat_table = ring_notify_get_chat_table(); - auto list = (GList *)g_hash_table_lookup(chat_table, cm); - if (list) - notification_old = (NotifyNotification *)list->data; - - /* we display chat notifications in different ways to suit different notification servers and - * their capabilities: - * 1. if the server doesn't support appending (eg: Notification Daemon) then we update the - * previous notification (if exists) with new text; otherwise it takes we have many - * notifications from the same person... we don't concatinate the old messages because - * servers which don't support append usually don't support multi line bodies - * 2. the notify-osd server supports appending; however it doesn't clear the old notifications - * on demand, which means in our case that chat messages which have already been read could - * still be displayed when a new notification is appended, thus in this case, we update - * the old notification body manually to only contain the unread messages - * 3. the 3rd case is that the server supports append but is not notify-osd, then we simply use - * the append feature - */ - - if (notification_old && !server_info.append) { - /* case 1 */ - notify_notification_update(notification_old, title, body, nullptr); - notification_new = notification_old; - } else if (notification_old && g_strcmp0(server_info.name, SERVER_NOTIFY_OSD) == 0) { - /* case 2 */ - /* print up to MAX_NOTIFICATIONS unread messages */ - int msg_count = 0; - auto idx_next = idx.sibling(idx.row() - 1, idx.column()); - auto read = idx_next.data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool(); - while (idx_next.isValid() && !read && msg_count < MAX_NOTIFICATIONS) { - - auto body_prev = body; - body = g_markup_printf_escaped("%s\n%s", body_prev, idx_next.data(Qt::DisplayRole).toString().toUtf8().constData()); - g_free(body_prev); - - idx_next = idx_next.sibling(idx_next.row() - 1, idx_next.column()); - read = idx_next.data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool(); - ++msg_count; +#if USE_LIBNOTIFY + std::shared_ptr<NotifyNotification> notification( + notify_notification_new(title.c_str(), body.c_str(), nullptr), g_object_unref); + priv->cpp->notifications_.emplace(id, notification); + + // Draw icon + auto firstLetter = name.empty() ? "" : QString(QString(name.c_str()).at(0)).toStdString(); // NOTE best way to be compatible with UTF-8 + auto default_avatar = Interfaces::PixbufManipulator().generateAvatar(firstLetter, uri); + auto photo = Interfaces::PixbufManipulator().scaleAndFrame(default_avatar.get(), QSize(50, 50)); + if (!icon.empty()) { + QByteArray byteArray(icon.c_str(), icon.length()); + QVariant avatar = Interfaces::PixbufManipulator().personPhoto(byteArray); + auto pixbuf_photo = Interfaces::PixbufManipulator().scaleAndFrame(avatar.value<std::shared_ptr<GdkPixbuf>>().get(), QSize(50, 50)); + if (avatar.isValid()) { + photo = pixbuf_photo; } + } + notify_notification_set_image_from_pixbuf(notification.get(), photo.get()); - notify_notification_update(notification_old, title, body, nullptr); - - notification_new = notification_old; + if (type != NotificationType::CHAT) { + notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL); + notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT); } else { - /* need new notification for case 1, 2, or 3 */ - notification_new = notify_notification_new(title, body, nullptr); - - /* track in hash table */ - auto list = (GList *)g_hash_table_lookup(chat_table, cm); - list = g_list_append(list, notification_new); - g_hash_table_replace(chat_table, cm, list); - - /* get photo */ - QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto( - cm, QSize(50, 50), false); - std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>(); - notify_notification_set_image_from_pixbuf(notification_new, photo.get()); - - /* normal priority for messages */ - notify_notification_set_urgency(notification_new, NOTIFY_URGENCY_NORMAL); - - /* remove the key and value from the hash table once the notification is - * closed; note that this will also unref the notification */ - g_signal_connect(notification_new, "closed", G_CALLBACK(notification_closed), cm); - - /* if the notification server supports actions, make the default action to show the chat view */ - if (server_info.actions) { - notify_notification_add_action(notification_new, - "default", - C_("notification action name", "Show"), - (NotifyActionCallback)ring_notify_show_cm, - cm, - nullptr); - } + notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_NORMAL); } - GError *error = nullptr; - success = notify_notification_show(notification_new, &error); - #if USE_CANBERRA - auto status = ca_context_play(ca_gtk_context_get(), - 0, - CA_PROP_MEDIA_FILENAME, - NOTIFICATION_FILE, - NULL); - if (status != 0) - g_warning("ca_context_play: %s", ca_strerror(status)); -#endif // USE_CANBERRA - - if (!success) { - g_warning("failed to show notification: %s", error->message); - g_clear_error(&error); + if (type != NotificationType::CALL) { + auto status = ca_context_play(ca_gtk_context_get(), + 0, + CA_PROP_MEDIA_FILENAME, + NOTIFICATION_FILE, + nullptr); + if (status != 0) + g_warning("ca_context_play: %s", ca_strerror(status)); } +#endif // USE_CANBERRA - g_free(title); - g_free(body); - - return success; -} - -static gboolean -show_message_if_unread(const QModelIndex *idx) -{ - g_return_val_if_fail(idx && idx->isValid(), G_SOURCE_REMOVE); - - if (!idx->data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool()) { - auto cm = idx->data(static_cast<int>(Media::TextRecording::Role::ContactMethod)).value<ContactMethod *>(); - ring_notify_show_text_message(cm, *idx); + // if the notification server supports actions, make the default action to show the chat view + if (priv->serverInfo.actions) { + if (type != NotificationType::CALL) { + notify_notification_add_action(notification.get(), + id.c_str(), + C_("", "Open conversation"), + (NotifyActionCallback)show_chat_view, + view, + nullptr); + if (type != NotificationType::CHAT) { + auto addId = "add:" + id; + notify_notification_add_action(notification.get(), + addId.c_str(), + C_("", "Accept"), + (NotifyActionCallback)accept_pending, + view, + nullptr); + auto rmId = "rm:" + id; + notify_notification_add_action(notification.get(), + rmId.c_str(), + C_("", "Refuse"), + (NotifyActionCallback)refuse_pending, + view, + nullptr); + } + } else { + auto acceptId = "accept:" + id; + notify_notification_add_action(notification.get(), + acceptId.c_str(), + C_("", "Accept"), + (NotifyActionCallback)accept_call, + view, + nullptr); + auto declineId = "decline:" + id; + notify_notification_add_action(notification.get(), + declineId.c_str(), + C_("", "Decline"), + (NotifyActionCallback)decline_call, + view, + nullptr); + } } - return G_SOURCE_REMOVE; -} + GError *error = nullptr; + success = notify_notification_show(notification.get(), &error); -static void -delete_idx(QModelIndex *idx) -{ - if (idx) { - delete idx; - idx = nullptr; + if (error) { + g_warning("failed to show notification: %s", error->message); + g_clear_error(&error); } -} - -#endif - -void -ring_notify_message( -#if !USE_LIBNOTIFY - ContactMethod*, Media::TextRecording*) -#else - ContactMethod *cm, Media::TextRecording *t) -#endif -{ - -#if USE_LIBNOTIFY - g_return_if_fail(cm && t); - - // get the message - auto model = t->instantMessagingModel(); - auto msg_idx = model->index(model->rowCount()-1, 0); - - ring_notify_show_text_message(cm, msg_idx); - - return; #endif + return success; } gboolean -ring_notify_close_chat_notification( -#if !USE_LIBNOTIFY - G_GNUC_UNUSED -#endif - ContactMethod *cm) +ring_hide_notification(RingNotifier* view, const std::string& id) { - gboolean notification_existed = FALSE; + g_return_val_if_fail(IS_RING_NOTIFIER(view), false); + gboolean success = FALSE; + RingNotifierPrivate *priv = RING_NOTIFIER_GET_PRIVATE(view); #if USE_LIBNOTIFY - /* checks if there exists a chat notification associated with the given ContactMethod - * and tries to close it; if it did exist, then the function returns TRUE */ - g_return_val_if_fail(cm, FALSE); - - - auto chat_table = ring_notify_get_chat_table(); - - if (auto list = (GList *)g_hash_table_lookup(chat_table, cm)) { - while (list) { - notification_existed = TRUE; - auto notification = (NotifyNotification *)list->data; - - GError *error = NULL; - if (!notify_notification_close(notification, &error)) { - g_warning("could not close notification: %s", error->message); - g_clear_error(&error); - } - - list = g_list_next(list); - } + // Search + auto notification = priv->cpp->notifications_.find(id); + if (notification == priv->cpp->notifications_.end()) { + return success; } - + // Close + GError *error = nullptr; + if (!notify_notification_close(notification->second.get(), &error)) { + g_warning("could not close notification: %s", error->message); + g_clear_error(&error); + } + // Erase + priv->cpp->notifications_.erase(id); #endif - - return notification_existed; + return success; } diff --git a/src/ringnotify.h b/src/ringnotify.h index 22cfb38b..3619cb53 100644 --- a/src/ringnotify.h +++ b/src/ringnotify.h @@ -1,6 +1,7 @@ /* * Copyright (C) 2015-2018 Savoir-faire Linux Inc. * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> + * Author: Sebastien Blin <sebastien.blin@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,22 +22,37 @@ #define RING_NOTIFY_H_ #include <gtk/gtk.h> -#include "ringmainwindow.h" - -class Call; -class ContactMethod; -namespace Media { -class TextRecording; -} +#include <string> G_BEGIN_DECLS +#define RING_NOTIFIER_TYPE (ring_notifier_get_type ()) +#define RING_NOTIFIER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), RING_NOTIFIER_TYPE, RingNotifier)) +#define RING_NOTIFIER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), RING_NOTIFIER_TYPE, RingNotifierClass)) +#define IS_RING_NOTIFIER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), RING_NOTIFIER_TYPE)) +#define IS_RING_NOTIFIER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), RING_NOTIFIER_TYPE)) + +typedef struct _RingNotifier RingNotifier; +typedef struct _RingNotifierClass RingNotifierClass; + +enum class NotificationType +{ + CALL, + REQUEST, + CHAT +}; + +GType ring_notifier_get_type (void) G_GNUC_CONST; +GtkWidget* ring_notifier_new (void); -void ring_notify_init(); -void ring_notify_uninit(); -gboolean ring_notify_is_initted(); -gboolean ring_notify_incoming_call(Call*); -void ring_notify_message(ContactMethod*, Media::TextRecording*); -gboolean ring_notify_close_chat_notification(ContactMethod*); +gboolean ring_show_notification(RingNotifier* view, + const std::string& icon, + const std::string& uri, + const std::string& name, + const std::string& id, + const std::string& title, + const std::string& body, + NotificationType type); +gboolean ring_hide_notification(RingNotifier* view, const std::string& id); G_END_DECLS diff --git a/ui/generalsettingsview.ui b/ui/generalsettingsview.ui index 326c2fe1..55b09f53 100644 --- a/ui/generalsettingsview.ui +++ b/ui/generalsettingsview.ui @@ -89,6 +89,14 @@ <property name="draw_indicator">True</property> </object> </child> + <child> + <object class="GtkCheckButton" id="checkbutton_pendingnotifications"> + <property name="label" translatable="yes">Enable notifications for pending requests.</property> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + </child> <child> <object class="GtkCheckButton" id="checkbutton_chatnotifications"> <property name="label" translatable="yes">Enable notifications for new chat messages.</property> -- GitLab