From 87a4602b9ac2fc2f6c631e3027377689c033b40a Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Mon, 14 Sep 2020 13:04:57 -0400
Subject: [PATCH] notifications: implement incoming message notifications

- Note: adds forcing the main window to the top to overcome a
  Qt/QML bug which prevents this from working when activating
  from a notification window source.

Change-Id: Ib5a93d3937fe5a4de0229f7aeae8469d1ffdfba3
---
 jami-qt.pro                                   |  1 -
 src/MainApplicationWindow.qml                 | 28 ++++++-
 src/accountadapter.cpp                        | 55 +++++++------
 src/accountadapter.h                          | 21 +++--
 src/accountlistmodel.h                        |  2 +
 src/calladapter.cpp                           | 82 +++++++++----------
 src/calladapter.h                             |  5 +-
 src/conversationsadapter.cpp                  | 72 ++++++++++++++--
 src/conversationsadapter.h                    |  4 +
 src/globalsystemtray.cpp                      | 45 ----------
 src/globalsystemtray.h                        | 28 +++----
 src/lrcinstance.h                             | 12 +++
 src/mainapplication.cpp                       | 22 ++++-
 src/mainview/MainView.qml                     |  5 ++
 src/mainview/components/AccountComboBox.qml   | 28 +++----
 .../components/ConversationSmartListView.qml  |  7 +-
 src/mainview/components/SidePanel.qml         |  9 +-
 src/messagesadapter.cpp                       | 19 ++---
 src/qtutils.h                                 |  6 +-
 src/utils.cpp                                 | 48 ++++++-----
 src/utils.h                                   | 38 ++++-----
 21 files changed, 306 insertions(+), 231 deletions(-)
 delete mode 100644 src/globalsystemtray.cpp

diff --git a/jami-qt.pro b/jami-qt.pro
index afc1348fc..d3933c6e7 100644
--- a/jami-qt.pro
+++ b/jami-qt.pro
@@ -162,7 +162,6 @@ SOURCES += \
         src/runguard.cpp \
         src/webchathelpers.cpp \
         src/main.cpp \
-        src/globalsystemtray.cpp \
         src/smartlistmodel.cpp \
         src/utils.cpp \
         src/pixbufmanipulator.cpp \
diff --git a/src/MainApplicationWindow.qml b/src/MainApplicationWindow.qml
index 695328f9c..8d0da0d6f 100644
--- a/src/MainApplicationWindow.qml
+++ b/src/MainApplicationWindow.qml
@@ -145,11 +145,31 @@ ApplicationWindow {
     Connections {
         target: LRCInstance
 
+        function restore(window) {
+            window.show()
+            window.raise();
+            window.requestActivate()
+        }
+
         function onRestoreAppRequested() {
-            if (mainViewLoader.item)
-                mainViewLoader.item.show()
-            else
-                wizardView.show()
+            var window = mainViewLoader.item ? mainViewLoader.item : wizardView
+            restore(window)
+        }
+
+        function onNotificationClicked(forceToTop) {
+            var window = mainViewLoader.item ? mainViewLoader.item : wizardView
+            // This is a hack to bring the window to the front which is normally done
+            // with QWindow::requestActivate but is thwarted for qml windows by the
+            // notification being clicked. Native solutions are preferable.
+            if (forceToTop && (!window.visible
+                    || window.visibility & Qt.WindowMinimized
+                    || window.visibility === Qt.WindowNoState)) {
+                var tmpFlags = window.flags
+                window.hide()
+                window.flags = Qt.WindowStaysOnTopHint
+                window.flags = tmpFlags
+            }
+            restore(window)
         }
     }
 }
diff --git a/src/accountadapter.cpp b/src/accountadapter.cpp
index 46d9172c1..a516f56d8 100644
--- a/src/accountadapter.cpp
+++ b/src/accountadapter.cpp
@@ -35,7 +35,16 @@ AccountAdapter::AccountAdapter(QObject* parent)
 void
 AccountAdapter::safeInit()
 {
-    setSelectedAccountId(LRCInstance::getCurrAccId());
+    connect(&LRCInstance::instance(),
+            &LRCInstance::currentAccountChanged,
+            this,
+            &AccountAdapter::onCurrentAccountChanged);
+
+    backToWelcomePage();
+
+    auto accountId = LRCInstance::getCurrAccId();
+    setProperties(accountId);
+    connectAccount(accountId);
 }
 
 lrc::api::NewAccountModel*
@@ -60,8 +69,10 @@ void
 AccountAdapter::accountChanged(int index)
 {
     auto accountList = LRCInstance::accountModel().getAccountList();
-    if (accountList.size() > index)
-        setSelectedAccountId(accountList.at(index));
+    if (accountList.size() > index) {
+        LRCInstance::setSelectedAccountId(accountList.at(index));
+        backToWelcomePage();
+    }
 }
 
 void
@@ -121,8 +132,6 @@ AccountAdapter::createJamiAccount(QString registeredName,
             } else {
                 LRCInstance::setAvatarForAccount(QPixmap::fromImage(avatarImg), accountId);
             }
-
-
         });
 
     connectFailure();
@@ -287,6 +296,14 @@ AccountAdapter::setSelectedConvId(const QString& convId)
     LRCInstance::setSelectedConvId(convId);
 }
 
+void
+AccountAdapter::onCurrentAccountChanged()
+{
+    auto accountId = LRCInstance::getCurrAccId();
+    setProperties(accountId);
+    connectAccount(accountId);
+}
+
 bool
 AccountAdapter::hasPassword()
 {
@@ -329,23 +346,6 @@ AccountAdapter::passwordSetStatusMessageBox(bool success, QString title, QString
     }
 }
 
-void
-AccountAdapter::setSelectedAccountId(const QString& accountId)
-{
-    LRCInstance::setSelectedAccountId(accountId);
-
-    setProperty("currentAccountId", accountId);
-    auto accountType = LRCInstance::getAccountInfo(accountId).profileInfo.type;
-    setProperty("currentAccountType", lrc::api::profile::to_string(accountType));
-
-    connectAccount(accountId);
-
-    emit contactModelChanged();
-    emit deviceModelChanged();
-
-    backToWelcomePage();
-}
-
 void
 AccountAdapter::backToWelcomePage()
 {
@@ -366,7 +366,6 @@ AccountAdapter::deselectConversation()
         return;
     }
 
-    currentConversationModel->selectConversation("");
     LRCInstance::setSelectedConvId();
 }
 
@@ -428,3 +427,13 @@ AccountAdapter::connectAccount(const QString& accountId)
         qWarning() << "Couldn't get account: " << accountId;
     }
 }
+
+void
+AccountAdapter::setProperties(const QString& accountId)
+{
+    setProperty("currentAccountId", accountId);
+    auto accountType = LRCInstance::getAccountInfo(accountId).profileInfo.type;
+    setProperty("currentAccountType", lrc::api::profile::to_string(accountType));
+    emit contactModelChanged();
+    emit deviceModelChanged();
+}
diff --git a/src/accountadapter.h b/src/accountadapter.h
index da7d0949f..0f7ff21e1 100644
--- a/src/accountadapter.h
+++ b/src/accountadapter.h
@@ -32,10 +32,8 @@ class AccountAdapter final : public QmlAdapterBase
     Q_OBJECT
 
     Q_PROPERTY(lrc::api::NewAccountModel* model READ getModel NOTIFY modelChanged)
-
     Q_PROPERTY(lrc::api::ContactModel* contactModel READ getContactModel NOTIFY contactModelChanged)
     Q_PROPERTY(lrc::api::NewDeviceModel* deviceModel READ getDeviceModel NOTIFY deviceModelChanged)
-
     Q_PROPERTY(QString currentAccountId MEMBER currentAccountId_ NOTIFY currentAccountIdChanged)
     Q_PROPERTY(lrc::api::profile::Type currentAccountType MEMBER currentAccountType_ NOTIFY
                    currentAccountTypeChanged)
@@ -62,6 +60,7 @@ public:
 protected:
     void safeInit() override;
 
+public:
     /*
      * Change to account corresponding to combox box index.
      */
@@ -105,7 +104,6 @@ protected:
     Q_INVOKABLE bool hasVideoCall();
     Q_INVOKABLE bool isPreviewing();
     Q_INVOKABLE void setCurrAccDisplayName(const QString& text);
-    Q_INVOKABLE void setSelectedAccountId(const QString& accountId = {});
     Q_INVOKABLE void setSelectedConvId(const QString& convId = {});
 
 signals:
@@ -121,10 +119,13 @@ signals:
     void navigateToWelcomePageRequested();
     void accountAdded(bool showBackUp, int index);
 
+private slots:
+    void onCurrentAccountChanged();
+
 private:
-    QString currentAccountId_;
-    lrc::api::profile::Type currentAccountType_;
-    int accountListSize_;
+    QString currentAccountId_ {};
+    lrc::api::profile::Type currentAccountType_ {};
+    int accountListSize_ {};
 
     void backToWelcomePage();
     void deselectConversation();
@@ -133,8 +134,14 @@ private:
      * Make account signal connections.
      */
     void connectAccount(const QString& accountId);
+
+    /*
+     * Make account signal connections.
+     */
+    void setProperties(const QString& accountId);
+
     /*
-     * Implement what to do when creat accout fails.
+     * Implement what to do when account creation fails.
      */
     void connectFailure();
 
diff --git a/src/accountlistmodel.h b/src/accountlistmodel.h
index 976d116e3..6741871d1 100644
--- a/src/accountlistmodel.h
+++ b/src/accountlistmodel.h
@@ -31,6 +31,7 @@ class AccountListModel : public QAbstractListModel
 
 public:
     enum Role { Alias = Qt::UserRole + 1, Username, Picture, Type, Status, ID };
+    Q_ENUM(Role)
 
     explicit AccountListModel(QObject* parent = 0);
     ~AccountListModel();
@@ -41,6 +42,7 @@ public:
     int rowCount(const QModelIndex& parent = QModelIndex()) const override;
     int columnCount(const QModelIndex& parent) const override;
     QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+
     /*
      * Override role name as access point in qml.
      */
diff --git a/src/calladapter.cpp b/src/calladapter.cpp
index 58a125ab1..e330cd5c8 100644
--- a/src/calladapter.cpp
+++ b/src/calladapter.cpp
@@ -109,9 +109,8 @@ CallAdapter::slotShowIncomingCallView(const QString& accountId, const conversati
     auto selectedAccountId = LRCInstance::getCurrAccId();
     auto* callModel = LRCInstance::getCurrentCallModel();
     if (!callModel->hasCall(convInfo.callId)) {
-
         if (QApplication::focusObject() == nullptr || accountId != selectedAccountId) {
-            showNotification(accountId, convInfo);
+            showNotification(accountId, convInfo.uid);
             return;
         }
 
@@ -125,16 +124,16 @@ CallAdapter::slotShowIncomingCallView(const QString& accountId, const conversati
         // Check INCOMING / OUTGOING call in current conversation
         if (currentConvHasCall) {
             auto currentCall = callModel->getCall(currentConvInfo.callId);
-            if (currentCall.status == lrc::api::call::Status::CONNECTED ||
-                    currentCall.status == lrc::api::call::Status::IN_PROGRESS) {
-                showNotification(accountId, convInfo);
+            if (currentCall.status == lrc::api::call::Status::CONNECTED
+                || currentCall.status == lrc::api::call::Status::IN_PROGRESS) {
+                showNotification(accountId, convInfo.uid);
                 return;
             }
         }
         emit incomingCallNeedToSetupMainView(accountId, convInfo.uid);
         emit showIncomingCallPage(accountId, convInfo.uid);
         emit showCallStack(accountId, convInfo.uid, true);
-        emit updateConversationSmartList();
+        emit LRCInstance::instance().updateSmartList();
         return;
     }
 
@@ -150,10 +149,9 @@ CallAdapter::slotShowIncomingCallView(const QString& accountId, const conversati
         auto showIncomingCall = false;
         auto accountProperties = LRCInstance::accountModel().getAccountConfig(selectedAccountId);
         if (!accountProperties.autoAnswer && !accountProperties.isRendezVous) {
-
             // App not focused or in different account
             if (QApplication::focusObject() == nullptr || accountId != selectedAccountId) {
-                showNotification(accountId, convInfo);
+                showNotification(accountId, convInfo.uid);
                 return;
             }
 
@@ -169,7 +167,7 @@ CallAdapter::slotShowIncomingCallView(const QString& accountId, const conversati
                 if (currentConvHasCall) {
                     auto currentCall = callModel->getCall(currentConvInfo.callId);
                     if (currentCall.status == lrc::api::call::Status::OUTGOING_RINGING) {
-                        showNotification(accountId, convInfo);
+                        showNotification(accountId, convInfo.uid);
                     } else {
                         showIncomingCall = true;
                     }
@@ -179,9 +177,9 @@ CallAdapter::slotShowIncomingCallView(const QString& accountId, const conversati
             } else { // Not current conversation
                 if (currentConvHasCall) {
                     auto currentCall = callModel->getCall(currentConvInfo.callId);
-                    if (currentCall.status == lrc::api::call::Status::CONNECTED ||
-                            currentCall.status == lrc::api::call::Status::IN_PROGRESS) {
-                        showNotification(accountId, convInfo);
+                    if (currentCall.status == lrc::api::call::Status::CONNECTED
+                        || currentCall.status == lrc::api::call::Status::IN_PROGRESS) {
+                        showNotification(accountId, convInfo.uid);
                         return;
                     }
                 }
@@ -196,7 +194,7 @@ CallAdapter::slotShowIncomingCallView(const QString& accountId, const conversati
         }
     }
     emit callStatusChanged(static_cast<int>(call.status), accountId, convInfo.uid);
-    emit updateConversationSmartList();
+    emit LRCInstance::instance().updateSmartList();
 }
 
 void
@@ -300,45 +298,39 @@ CallAdapter::getConferencesInfos()
 }
 
 void
-CallAdapter::showNotification(const QString& accountId, const lrc::api::conversation::Info& convInfo)
+CallAdapter::showNotification(const QString& accountId, const QString& convUid)
 {
-    // Hack for handling multiple consecutive calls to slotShowIncomingCallView (bug)
-    // Do not set notification if it is already active for the account and conversation
-    if (accountId == GlobalSystemTray::instance().getTriggeredAccountId() &&
-            convInfo.uid == GlobalSystemTray::instance().getPossibleOnGoingConversationUid()) {
-        return;
-    }
-
-    QString sender = convInfo.uid;
-    if (accountId != "") {
+    QString from {};
+    auto convInfo = LRCInstance::getConversationFromConvUid(convUid, accountId);
+    if (!accountId.isEmpty() && !convInfo.uid.isEmpty()) {
         auto& accInfo = LRCInstance::getAccountInfo(accountId);
         if (!convInfo.participants.isEmpty()) {
-            auto &contact = accInfo.contactModel->getContact(convInfo.participants[0]);
-            sender = Utils::bestNameForContact(contact);
+            auto& contact = accInfo.contactModel->getContact(convInfo.participants[0]);
+            from = Utils::bestNameForContact(contact);
         }
     }
 
-    GlobalSystemTray::instance().setPossibleOnGoingConversationUid(convInfo.uid);
-
-    QObject::connect(&GlobalSystemTray::instance(), &GlobalSystemTray::messageClicked,
-                     this, [this, accountId, convInfo]() {
-        if (accountId != "" && convInfo.uid != "") {
-
-            emit incomingCallNeedToSetupMainView(accountId, convInfo.uid, true);
-
-            auto call = LRCInstance::getCallInfoForConversation(convInfo);
-            if (call->status == lrc::api::call::Status::INCOMING_RINGING) {
-                emit showIncomingCallPage(accountId, convInfo.uid);
-                emit showCallStack(accountId, convInfo.uid, true);
-            }
-            emit updateConversationSmartList();
+    auto onClicked = [this, accountId, convInfo]() {
+#ifdef Q_OS_WINDOWS
+        emit LRCInstance::instance().notificationClicked();
+#else
+        emit LRCInstance::instance().notificationClicked(true);
+#endif
+        // TODO: only a selectConversation(accountId, convUid) should occur
+        // here, the UI should resolve its state by observation as much as possible,
+        // or in response to a conversationSelected signal.
+        // - https://git.jami.net/savoirfairelinux/jami-client-qt/issues/87
+        // - https://git.jami.net/savoirfairelinux/jami-client-qt/issues/86
+        emit incomingCallNeedToSetupMainView(accountId, convInfo.uid);
+        auto call = LRCInstance::getCallInfoForConversation(convInfo);
+        if (call && call->status == lrc::api::call::Status::INCOMING_RINGING) {
+            emit showIncomingCallPage(accountId, convInfo.uid);
+            emit showCallStack(accountId, convInfo.uid, true);
         }
-        GlobalSystemTray::instance().setTriggeredAccountId("");
-        GlobalSystemTray::instance().setPossibleOnGoingConversationUid("");
-
-    }, Qt::UniqueConnection);
+        emit LRCInstance::instance().updateSmartList();
+    };
 
-    Utils::showSystemNotification(QApplication::focusWidget(), sender, tr("is calling you"), 0, accountId);
+    Utils::showNotification(tr("is calling you"), from, accountId, convUid, onClicked);
 }
 
 void
@@ -472,7 +464,7 @@ CallAdapter::connectCallModel(const QString& accountId)
                 break;
             }
 
-            emit updateConversationSmartList();
+            emit LRCInstance::instance().updateSmartList();
         });
 }
 
diff --git a/src/calladapter.h b/src/calladapter.h
index cfc18edc9..6fea6b3be 100644
--- a/src/calladapter.h
+++ b/src/calladapter.h
@@ -78,7 +78,8 @@ signals:
                                  const QString& accountId,
                                  const QString& callId);
 
-    void incomingCallNeedToSetupMainView(const QString& accountId, const QString& convUid,
+    void incomingCallNeedToSetupMainView(const QString& accountId,
+                                         const QString& convUid,
                                          bool fromNotification = false);
     void previewVisibilityNeedToChange(bool visible);
 
@@ -107,7 +108,7 @@ private:
                     const QString& accountId = {},
                     bool forceCallOnly = false);
     bool shouldShowPreview(bool force);
-    void showNotification(const QString& accountId, const lrc::api::conversation::Info& convInfo);
+    void showNotification(const QString& accountId, const QString& convUid);
 
     /*
      * Current conf/call info.
diff --git a/src/conversationsadapter.cpp b/src/conversationsadapter.cpp
index e52fb0070..2dcedda88 100644
--- a/src/conversationsadapter.cpp
+++ b/src/conversationsadapter.cpp
@@ -24,6 +24,9 @@
 #include "conversationsadapter.h"
 
 #include "utils.h"
+#include "qtutils.h"
+
+#include <QApplication>
 
 ConversationsAdapter::ConversationsAdapter(QObject* parent)
     : QmlAdapterBase(parent)
@@ -42,6 +45,11 @@ ConversationsAdapter::safeInit()
                 emit showChatView(accountId, convInfo.uid);
             });
 
+    connect(&LRCInstance::behaviorController(),
+            &BehaviorController::newUnreadInteraction,
+            this,
+            &ConversationsAdapter::onNewUnreadInteraction);
+
     connect(&LRCInstance::instance(),
             &LRCInstance::currentAccountChanged,
             this,
@@ -88,20 +96,33 @@ ConversationsAdapter::selectConversation(const QString& convUid)
 }
 
 bool
-ConversationsAdapter::selectConversation(const lrc::api::conversation::Info& item,
+ConversationsAdapter::selectConversation(const lrc::api::conversation::Info& convInfo,
                                          bool preventSendingSignal)
 {
     // accInfo.conversationModel->selectConversation(item.uid) only emit ui
     // behavior control signals, but sometimes we do not want that,
     // preventSendingSignal boolean can help us to determine.
-    if (LRCInstance::getCurrentConvUid() == item.uid) {
+    if (LRCInstance::getCurrentConvUid() == convInfo.uid
+        && LRCInstance::getCurrAccId() == convInfo.accountId) {
         return false;
-    } else if (item.participants.size() > 0) {
-        auto& accInfo = LRCInstance::getAccountInfo(item.accountId);
-        LRCInstance::setSelectedConvId(item.uid);
-        if (!preventSendingSignal)
-            accInfo.conversationModel->selectConversation(item.uid);
-        accInfo.conversationModel->clearUnreadInteractions(item.uid);
+    } else if (convInfo.participants.size() > 0) {
+        // If the account is not currently selected, do that first, then
+        // proceed to select the conversation.
+        auto selectConversation = [convInfo, preventSendingSignal] {
+            auto& accInfo = LRCInstance::getAccountInfo(convInfo.accountId);
+            LRCInstance::setSelectedConvId(convInfo.uid);
+            if (!preventSendingSignal)
+                accInfo.conversationModel->selectConversation(convInfo.uid);
+            accInfo.conversationModel->clearUnreadInteractions(convInfo.uid);
+        };
+        if (convInfo.accountId != LRCInstance::getCurrAccId()) {
+            Utils::oneShotConnect(&LRCInstance::instance(),
+                                  &LRCInstance::currentAccountChanged,
+                                  [selectConversation] { selectConversation(); });
+            LRCInstance::setSelectedAccountId(convInfo.accountId);
+        } else {
+            selectConversation();
+        }
         return true;
     }
 }
@@ -138,6 +159,38 @@ ConversationsAdapter::onCurrentAccountIdChanged()
     connectConversationModel();
 }
 
+void
+ConversationsAdapter::onNewUnreadInteraction(const QString& accountId,
+                                             const QString& convUid,
+                                             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& contact = accInfo.contactModel->getContact(interaction.authorUri);
+        auto from = Utils::bestNameForContact(contact);
+        auto onClicked = [this, accountId, convUid, uri = interaction.authorUri] {
+#ifdef Q_OS_WINDOWS
+            emit LRCInstance::instance().notificationClicked();
+#else
+            emit LRCInstance::instance().notificationClicked(true);
+#endif
+            auto convInfo = LRCInstance::getConversationFromConvUid(convUid, accountId);
+            if (!convInfo.uid.isEmpty()) {
+                selectConversation(convInfo, false);
+                emit LRCInstance::instance().updateSmartList();
+                emit modelSorted(uri);
+            }
+        };
+
+        Utils::showNotification(interaction.body, from, accountId, convUid, onClicked);
+        return;
+    }
+}
+
 void
 ConversationsAdapter::updateConversationsFilterWidget()
 {
@@ -282,8 +335,11 @@ ConversationsAdapter::disconnectConversationModel()
     QObject::disconnect(modelSortedConnection_);
     QObject::disconnect(modelUpdatedConnection_);
     QObject::disconnect(filterChangedConnection_);
+    QObject::disconnect(newConversationConnection_);
     QObject::disconnect(conversationRemovedConnection_);
     QObject::disconnect(conversationClearedConnection);
+    QObject::disconnect(selectedCallChanged_);
+    QObject::disconnect(smartlistSelectionConnection_);
     QObject::disconnect(interactionRemovedConnection_);
     QObject::disconnect(searchStatusChangedConnection_);
     QObject::disconnect(searchResultUpdatedConnection_);
diff --git a/src/conversationsadapter.h b/src/conversationsadapter.h
index a2b962459..8b6ebdc61 100644
--- a/src/conversationsadapter.h
+++ b/src/conversationsadapter.h
@@ -59,6 +59,10 @@ signals:
 
 private slots:
     void onCurrentAccountIdChanged();
+    void onNewUnreadInteraction(const QString& accountId,
+                                const QString& convUid,
+                                uint64_t interactionId,
+                                const interaction::Info& interaction);
 
 private:
     void setConversationFilter(lrc::api::profile::Type filter);
diff --git a/src/globalsystemtray.cpp b/src/globalsystemtray.cpp
deleted file mode 100644
index 73b9fddbb..000000000
--- a/src/globalsystemtray.cpp
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2015-2020 by Savoir-faire Linux
- * Author: Edric Ladent Milaret <edric.ladent-milaret@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
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include "globalsystemtray.h"
-
-GlobalSystemTray::GlobalSystemTray() {}
-
-void
-GlobalSystemTray::setTriggeredAccountId(const QString& accountId)
-{
-    triggeredAccountId_ = accountId;
-}
-
-const QString&
-GlobalSystemTray::getTriggeredAccountId()
-{
-    return triggeredAccountId_;
-}
-
-void
-GlobalSystemTray::setPossibleOnGoingConversationUid(const QString& convUid)
-{
-    triggeredOnGoingConvUid_ = convUid;
-}
-
-const QString&
-GlobalSystemTray::getPossibleOnGoingConversationUid()
-{
-    return triggeredOnGoingConvUid_;
-}
diff --git a/src/globalsystemtray.h b/src/globalsystemtray.h
index 1ce513c29..e3853306d 100644
--- a/src/globalsystemtray.h
+++ b/src/globalsystemtray.h
@@ -22,32 +22,32 @@
 
 #include <QSystemTrayIcon>
 
-class GlobalSystemTray : public QSystemTrayIcon
+class GlobalSystemTray final : public QSystemTrayIcon
 {
     Q_OBJECT
 
 public:
+    ~GlobalSystemTray() = default;
     static GlobalSystemTray& instance()
     {
         static GlobalSystemTray* instance_ = new GlobalSystemTray();
         return *instance_;
     }
 
-    /*
-     * Remember the last triggering account for the notification,
-     * safe since user cannot activate previous notifications.
-     */
-    void setTriggeredAccountId(const QString& accountId);
+    inline static QString notificationAccountId {};
+    inline static QString notificationConvUid {};
 
-    const QString& getTriggeredAccountId();
-
-    void setPossibleOnGoingConversationUid(const QString& convUid);
-
-    const QString& getPossibleOnGoingConversationUid();
+    template<typename Func>
+    static void connectClicked(Func&& onClicked)
+    {
+        auto& instance_ = instance();
+        instance_.disconnect(instance_.messageClicked_);
+        instance_.connect(&instance_, &QSystemTrayIcon::messageClicked, onClicked);
+    }
 
 private:
-    GlobalSystemTray();
+    explicit GlobalSystemTray()
+        : QSystemTrayIcon() {};
 
-    QString triggeredAccountId_;
-    QString triggeredOnGoingConvUid_;
+    QMetaObject::Connection messageClicked_;
 };
diff --git a/src/lrcinstance.h b/src/lrcinstance.h
index 811ff5687..145cb6bf5 100644
--- a/src/lrcinstance.h
+++ b/src/lrcinstance.h
@@ -226,6 +226,16 @@ public:
         return invalid;
     }
 
+    static const conversation::Info& getConversationFromConvUid(const QString& convUid,
+                                                                const QString& accountId = {},
+                                                                bool filtered = false)
+    {
+        return getConversation(
+            !accountId.isEmpty() ? accountId : getCurrAccId(),
+            [&](const conversation::Info& conv) -> bool { return convUid == conv.uid; },
+            filtered);
+    }
+
     static const conversation::Info& getConversationFromCallId(const QString& callId,
                                                                const QString& accountId = {},
                                                                bool filtered = false)
@@ -431,6 +441,8 @@ signals:
     void accountListChanged();
     void currentAccountChanged();
     void restoreAppRequested();
+    void notificationClicked(bool forceToTop = false);
+    void updateSmartList();
 
 private:
     LRCInstance(migrateCallback willMigrateCb = {}, migrateCallback didMigrateCb = {})
diff --git a/src/mainapplication.cpp b/src/mainapplication.cpp
index dc93b6b01..1c37de9e6 100644
--- a/src/mainapplication.cpp
+++ b/src/mainapplication.cpp
@@ -33,6 +33,7 @@
 #include <QFontDatabase>
 #include <QMenu>
 #include <QQmlContext>
+#include <QWindow>
 
 #include <locale.h>
 
@@ -133,8 +134,27 @@ MainApplication::init()
 
     GlobalInstances::setPixmapManipulator(std::make_unique<PixbufManipulator>());
     initLrc();
+
     initConnectivityMonitor();
 
+#ifdef Q_OS_WINDOWS
+    QObject::connect(&LRCInstance::instance(), &LRCInstance::notificationClicked, [] {
+        for (QWindow* appWindow : qApp->allWindows()) {
+            if (appWindow->objectName().compare("mainViewWindow"))
+                continue;
+            // clang-format off
+            ::SetWindowPos((HWND) appWindow->winId(),
+                           HWND_TOPMOST, 0, 0, 0, 0,
+                           SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
+            ::SetWindowPos((HWND) appWindow->winId(),
+                           HWND_NOTOPMOST, 0, 0, 0, 0,
+                           SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
+            // clang-format on
+            return;
+        }
+    });
+#endif
+
     bool startMinimized {false};
     parseArguments(startMinimized);
 
@@ -214,7 +234,7 @@ MainApplication::initConnectivityMonitor()
 {
 #ifdef Q_OS_WIN
     connectivityMonitor_.reset(new ConnectivityMonitor(this));
-    connect(connectivityMonitor_.get(), &ConnectivityMonitor::connectivityChanged, [this] {
+    connect(connectivityMonitor_.get(), &ConnectivityMonitor::connectivityChanged, [] {
         LRCInstance::connectivityChanged();
     });
 #endif // Q_OS_WIN
diff --git a/src/mainview/MainView.qml b/src/mainview/MainView.qml
index 47e6ecc5f..13a3acf79 100644
--- a/src/mainview/MainView.qml
+++ b/src/mainview/MainView.qml
@@ -32,6 +32,7 @@ import "../settingsview/components"
 
 Window {
     id: mainViewWindow
+    objectName: "mainViewWindow"
 
     property int minWidth: settingsViewPreferredWidth
     property int minHeight: 400
@@ -121,6 +122,7 @@ Window {
 
     function newAccountAdded(index) {
         mainViewWindowSidePanel.refreshAccountComboBox(index)
+        AccountAdapter.accountChanged(index)
     }
 
     function currentAccountIsCalling() {
@@ -230,6 +232,7 @@ Window {
             callStackView.updateCorrespondingUI()
 
             mainViewWindowSidePanel.refreshAccountComboBox(index)
+            AccountAdapter.accountChanged(index)
             ConversationsAdapter.selectConversation(accountId, convUid, !fromNotification)
             MessagesAdapter.setupChatView(convUid)
         }
@@ -304,6 +307,7 @@ Window {
                         mainViewWindowSidePanel.deselectConversationSmartList()
 
                         mainViewWindowSidePanel.refreshAccountComboBox(index)
+                        AccountAdapter.accountChanged(index)
 
                         settingsView.slotAccountListChanged()
                         settingsView.setSelected(settingsView.selectedMenu, true)
@@ -545,6 +549,7 @@ Window {
 
         onSettingsViewWindowNeedToShowMainViewWindow: {
             mainViewWindowSidePanel.refreshAccountComboBox(0)
+            AccountAdapter.accountChanged(index)
             toggleSettingsView()
         }
 
diff --git a/src/mainview/components/AccountComboBox.qml b/src/mainview/components/AccountComboBox.qml
index 127bfd5ce..54a7dc8ed 100644
--- a/src/mainview/components/AccountComboBox.qml
+++ b/src/mainview/components/AccountComboBox.qml
@@ -25,7 +25,7 @@ import net.jami.Adapters 1.0
 import "../../commoncomponents"
 
 ComboBox {
-    id: accountComboBox
+    id: root
 
     signal accountChanged(int index)
     signal needToBackToWelcomePage
@@ -61,9 +61,9 @@ ComboBox {
     Image {
         id: userImageRoot
 
-        anchors.left: accountComboBox.left
+        anchors.left: root.left
         anchors.leftMargin: 16
-        anchors.verticalCenter: accountComboBox.verticalCenter
+        anchors.verticalCenter: root.verticalCenter
 
         width: 30
         height: 30
@@ -130,7 +130,7 @@ ComboBox {
 
         font: textUserAliasRoot.font
         elide: Text.ElideRight
-        elideWidth: accountComboBox.width - userImageRoot.width - settingsButton.width
+        elideWidth: root.width - userImageRoot.width - settingsButton.width
                     - arrowDropDown.width - qrCodeGenerateButton.width - 55
 
         // Role::Alias
@@ -142,7 +142,7 @@ ComboBox {
 
         font: textUsernameRoot.font
         elide: Text.ElideRight
-        elideWidth: accountComboBox.width - userImageRoot.width - settingsButton.width
+        elideWidth: root.width - userImageRoot.width - settingsButton.width
                     - qrCodeGenerateButton.width - 55
 
 
@@ -156,7 +156,7 @@ ComboBox {
 
         anchors.right: settingsButton.left
         anchors.rightMargin: 10
-        anchors.verticalCenter: accountComboBox.verticalCenter
+        anchors.verticalCenter: root.verticalCenter
 
         buttonImageHeight: height - 8
         buttonImageWidth: width - 8
@@ -180,9 +180,9 @@ ComboBox {
     HoverableButton {
         id: settingsButton
 
-        anchors.right: accountComboBox.right
+        anchors.right: root.right
         anchors.rightMargin: 10
-        anchors.verticalCenter: accountComboBox.verticalCenter
+        anchors.verticalCenter: root.verticalCenter
 
         buttonImageHeight: height - 8
         buttonImageWidth: width - 8
@@ -207,8 +207,8 @@ ComboBox {
     background: Rectangle {
         id: rootItemBackground
 
-        implicitWidth: accountComboBox.width
-        implicitHeight: accountComboBox.height
+        implicitWidth: root.width
+        implicitHeight: root.height
         color: JamiTheme.backgroundColor
     }
 
@@ -240,9 +240,9 @@ ComboBox {
             } else {
                 rootItemBackground.color = JamiTheme.releaseColor
                 if (comboBoxPopup.opened) {
-                    accountComboBox.popup.close()
+                    root.popup.close()
                 } else {
-                    accountComboBox.popup.open()
+                    root.popup.open()
                 }
             }
         }
@@ -291,11 +291,11 @@ ComboBox {
         }
 
         onAccountNeedToChange: {
-            accountComboBox.accountChanged(index)
+            root.accountChanged(index)
         }
 
         onNewAccountButtonClicked: {
-            accountComboBox.newAccountButtonClicked()
+            root.newAccountButtonClicked()
         }
     }
 
diff --git a/src/mainview/components/ConversationSmartListView.qml b/src/mainview/components/ConversationSmartListView.qml
index db5c82c5b..c3df41f74 100644
--- a/src/mainview/components/ConversationSmartListView.qml
+++ b/src/mainview/components/ConversationSmartListView.qml
@@ -80,11 +80,8 @@ ListView {
     }
 
     Connections {
-        target: CallAdapter
-
-        function onUpdateConversationSmartList() {
-            updateListView()
-        }
+        target: LRCInstance
+        function onUpdateSmartList() { updateListView() }
     }
 
     onCurrentIndexChanged: {
diff --git a/src/mainview/components/SidePanel.qml b/src/mainview/components/SidePanel.qml
index 5ef32df94..e3eb40e9e 100644
--- a/src/mainview/components/SidePanel.qml
+++ b/src/mainview/components/SidePanel.qml
@@ -26,12 +26,19 @@ import "../../commoncomponents"
 
 Rectangle {
     id: sidePanelRect
+
     color: JamiTheme.backgroundColor
 
     property bool tabBarVisible: true
     property int pendingRequestCount: 0
     property int totalUnreadMessagesCount: 0
 
+    property string currentAccountId: AccountAdapter.currentAccountId
+    onCurrentAccountIdChanged: {
+        var index = UtilsAdapter.getCurrAccList().indexOf(currentAccountId)
+        refreshAccountComboBox(index)
+    }
+
     signal conversationSmartListNeedToAccessMessageWebView(string currentUserDisplayName, string currentUserAlias, string currentUID, bool callStackViewShouldShow, bool isAudioOnly, int callState)
     signal accountComboBoxNeedToShowWelcomePage()
     signal conversationSmartListViewNeedToShowWelcomePage
@@ -73,8 +80,6 @@ Rectangle {
     }
 
     function refreshAccountComboBox(index) {
-        AccountAdapter.accountChanged(index)
-
         accountComboBox.update()
         accountChangedUIReset()
         accountComboBox.resetAccountListModel()
diff --git a/src/messagesadapter.cpp b/src/messagesadapter.cpp
index 539d9c19a..7f2d85a0e 100644
--- a/src/messagesadapter.cpp
+++ b/src/messagesadapter.cpp
@@ -23,7 +23,9 @@
 
 #include "messagesadapter.h"
 
+#include "globalsystemtray.h"
 #include "qtutils.h"
+#include "utils.h"
 #include "webchathelpers.h"
 
 #include <QApplication>
@@ -469,22 +471,11 @@ MessagesAdapter::newInteraction(const QString& accountId,
 {
     Q_UNUSED(interactionId);
     try {
-        auto& accountInfo = LRCInstance::getAccountInfo(accountId);
-        auto& convModel = accountInfo.conversationModel;
-        const auto conversation = convModel->getConversationForUID(convUid);
-
-        if (conversation.uid.isEmpty()) {
-            return;
-        }
-        if (!interaction.authorUri.isEmpty()
-            && (!QApplication::focusWindow() || LRCInstance::getCurrAccId() != accountId)) {
-            /*
-             * TODO: Notification from other accounts.
-             */
-        }
-        if (convUid != LRCInstance::getCurrentConvUid()) {
+        if (convUid.isEmpty() || convUid != LRCInstance::getCurrentConvUid()) {
             return;
         }
+        auto& accountInfo = LRCInstance::getAccountInfo(accountId);
+        auto& convModel = accountInfo.conversationModel;
         convModel->clearUnreadInteractions(convUid);
         printNewInteraction(*convModel, interactionId, interaction);
     } catch (...) {
diff --git a/src/qtutils.h b/src/qtutils.h
index 577127b42..ba970047c 100644
--- a/src/qtutils.h
+++ b/src/qtutils.h
@@ -1,10 +1,10 @@
-/*
+/*!
  * Copyright (C) 2015-2020 by Savoir-faire Linux
  * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
  * Author: Isa Nanic <isa.nanic@savoirfairelinux.com>
- * Author: Mingrui Zhang   <mingrui.zhang@savoirfairelinux.com>
- * Author: Aline Gondim Santos   <aline.gondimsantos@savoirfairelinux.com>
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Author: Aline Gondim Santos <aline.gondimsantos@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
diff --git a/src/utils.cpp b/src/utils.cpp
index 7cfdc54c3..805d803f1 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -1,10 +1,10 @@
-/*
+/*!
  * Copyright (C) 2015-2020 by Savoir-faire Linux
  * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
  * Author: Isa Nanic <isa.nanic@savoirfairelinux.com
  * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
- * Author: Aline Gondim Santos   <aline.gondimsantos@savoirfairelinux.com>
+ * Author: Aline Gondim Santos <aline.gondimsantos@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
@@ -281,34 +281,38 @@ Utils::getCirclePhoto(const QImage original, int sizePhoto)
 }
 
 void
-Utils::showSystemNotification(QWidget* widget,
-                              const QString& message,
-                              long delay,
-                              const QString& triggeredAccountId)
+Utils::showNotification(const QString& message,
+                        const QString& from,
+                        const QString& accountId,
+                        const QString& convUid,
+                        std::function<void()> const& onClicked)
 {
+    if (accountId.isEmpty() || convUid.isEmpty()) {
+        // This should never happen.
+        qFatal("Invalid account or conversation.");
+    }
+
     if (!AppSettingsManager::getValue(Settings::Key::EnableNotifications).toBool()) {
         qWarning() << "Notifications are disabled";
         return;
     }
-    GlobalSystemTray::instance().setTriggeredAccountId(triggeredAccountId);
-    GlobalSystemTray::instance().showMessage(message, "", QIcon(":images/jami.png"));
-    QApplication::alert(widget, delay);
-}
 
-void
-Utils::showSystemNotification(QWidget* widget,
-                              const QString& sender,
-                              const QString& message,
-                              long delay,
-                              const QString& triggeredAccountId)
-{
-    if (!AppSettingsManager::getValue(Settings::Key::EnableNotifications).toBool()) {
-        qWarning() << "Notifications are disabled";
+    if (accountId == GlobalSystemTray::notificationAccountId
+        && convUid == GlobalSystemTray::notificationConvUid) {
+        GlobalSystemTray::notificationAccountId.clear();
+        GlobalSystemTray::notificationConvUid.clear();
         return;
     }
-    GlobalSystemTray::instance().setTriggeredAccountId(triggeredAccountId);
-    GlobalSystemTray::instance().showMessage(sender, message, QIcon(":images/jami.png"));
-    QApplication::alert(widget, delay);
+
+    GlobalSystemTray::connectClicked(std::move(onClicked));
+
+    GlobalSystemTray::notificationAccountId = accountId;
+    GlobalSystemTray::notificationConvUid = convUid;
+
+    if (from.isEmpty())
+        GlobalSystemTray::instance().showMessage(message, "", QIcon(":images/jami.png"));
+    else
+        GlobalSystemTray::instance().showMessage(from, message, QIcon(":images/jami.png"));
 }
 
 QSize
diff --git a/src/utils.h b/src/utils.h
index 2cec141cf..3bb7a08a2 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -1,10 +1,10 @@
-/*
+/*!
  * Copyright (C) 2015-2020 by Savoir-faire Linux
  * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
  * Author: Isa Nanic <isa.nanic@savoirfairelinux.com>
- * Author: Mingrui Zhang   <mingrui.zhang@savoirfairelinux.com>
- * Author: Aline Gondim Santos   <aline.gondimsantos@savoirfairelinux.com>
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Author: Aline Gondim Santos <aline.gondimsantos@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
@@ -45,7 +45,7 @@
 #include <windows.h>
 #undef ERROR
 #else
-#define LPCWSTR char *
+#define LPCWSTR char*
 #endif
 
 #include "api/account.h"
@@ -58,25 +58,21 @@ namespace Utils {
 /*
  * App/System
  */
-bool CreateStartupLink(const std::wstring &wstrAppName);
-void DeleteStartupLink(const std::wstring &wstrAppName);
+bool CreateStartupLink(const std::wstring& wstrAppName);
+void DeleteStartupLink(const std::wstring& wstrAppName);
 bool CreateLink(LPCWSTR lpszPathObj, LPCWSTR lpszPathLink);
 bool CheckStartupLink(const std::wstring& wstrAppName);
 const char* WinGetEnv(const char* name);
 QString GetRingtonePath();
 QString GenGUID();
 QString GetISODate();
-void showSystemNotification(QWidget* widget,
-                            const QString& message,
-                            long delay = 5000,
-                            const QString &triggeredAccountId = "");
-void showSystemNotification(QWidget *widget,
-                            const QString &sender,
-                            const QString &message,
-                            long delay = 5000,
-                            const QString &triggeredAccountId = "");
-QSize getRealSize(QScreen *screen);
-void forceDeleteAsync(const QString &path);
+void showNotification(const QString& message,
+                      const QString& from,
+                      const QString& accountId,
+                      const QString& convUid,
+                      std::function<void()> const& onClicked);
+QSize getRealSize(QScreen* screen);
+void forceDeleteAsync(const QString& path);
 QString getChangeLog();
 QString getProjectCredits();
 void removeOldVersions();
@@ -90,8 +86,8 @@ static constexpr bool isBeta = true;
 static constexpr bool isBeta = false;
 #endif
 void cleanUpdateFiles();
-void checkForUpdates(bool withUI, QWidget *parent = nullptr);
-void applyUpdates(bool updateToBeta, QWidget *parent = nullptr);
+void checkForUpdates(bool withUI, QWidget* parent = nullptr);
+void applyUpdates(bool updateToBeta, QWidget* parent = nullptr);
 
 /*
  * LRC helpers
@@ -118,8 +114,8 @@ bool getReplyMessageBox(QWidget* widget, const QString& title, const QString& te
 static const QSize defaultAvatarSize {128, 128};
 QString getContactImageString(const QString& accountId, const QString& uid);
 QImage getCirclePhoto(const QImage original, int sizePhoto);
-QImage conversationPhoto(const QString &convUid,
-                         const lrc::api::account::Info &accountInfo,
+QImage conversationPhoto(const QString& convUid,
+                         const lrc::api::account::Info& accountInfo,
                          bool filtered = false);
 QColor getAvatarColor(const QString& canonicalUri);
 QImage fallbackAvatar(const QString& canonicalUriStr,
-- 
GitLab