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