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