diff --git a/CMakeLists.txt b/CMakeLists.txt
index 77dc4ea31403d9a797ef30191f16b25023fad25d..a678b4eb5398d4ee5507cca1caff1aa1fe2e8fa0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -67,7 +67,11 @@ set(COMMON_SOURCES
     ${SRC_DIR}/screensaver.cpp
     ${SRC_DIR}/systemtray.cpp
     ${SRC_DIR}/appsettingsmanager.cpp
-    ${SRC_DIR}/lrcinstance.cpp)
+    ${SRC_DIR}/lrcinstance.cpp
+    ${SRC_DIR}/selectablelistproxymodel.cpp
+    ${SRC_DIR}/conversationlistmodelbase.cpp
+    ${SRC_DIR}/conversationlistmodel.cpp
+    ${SRC_DIR}/searchresultslistmodel.cpp)
 
 set(COMMON_HEADERS
     ${SRC_DIR}/avatarimageprovider.h
@@ -118,7 +122,11 @@ set(COMMON_HEADERS
     ${SRC_DIR}/screensaver.h
     ${SRC_DIR}/systemtray.h
     ${SRC_DIR}/appsettingsmanager.h
-    ${SRC_DIR}/lrcinstance.h)
+    ${SRC_DIR}/lrcinstance.h
+    ${SRC_DIR}/selectablelistproxymodel.h
+    ${SRC_DIR}/conversationlistmodelbase.h
+    ${SRC_DIR}/conversationlistmodel.h
+    ${SRC_DIR}/searchresultslistmodel.h)
 
 set(QML_LIBS
     Qt5::Quick
diff --git a/qml.qrc b/qml.qrc
index b2782a70a43f4ab2013d98ec691b5451c91c7464..15cbfbd1279f851e6dd23a287a00fc09a52494af 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -102,7 +102,6 @@
         <file>src/mainview/components/MessageWebView.qml</file>
         <file>src/mainview/components/MessageWebViewHeader.qml</file>
         <file>src/mainview/components/AccountComboBox.qml</file>
-        <file>src/mainview/components/ConversationSmartListView.qml</file>
         <file>src/mainview/components/CallStackView.qml</file>
         <file>src/mainview/components/IncomingCallPage.qml</file>
         <file>src/mainview/components/OutgoingCallPage.qml</file>
@@ -114,7 +113,6 @@
         <file>src/mainview/components/ParticipantOverlay.qml</file>
         <file>src/mainview/components/ProjectCreditsScrollView.qml</file>
         <file>src/mainview/components/AccountComboBoxPopup.qml</file>
-        <file>src/mainview/components/ConversationSmartListViewItemDelegate.qml</file>
         <file>src/mainview/components/SidePanelTabBar.qml</file>
         <file>src/mainview/components/WelcomePageQrDialog.qml</file>
         <file>src/mainview/components/ConversationSmartListContextMenu.qml</file>
@@ -138,5 +136,8 @@
         <file>src/mainview/js/pluginhandlerpickercreation.js</file>
         <file>src/mainview/components/FilterTabButton.qml</file>
         <file>src/mainview/components/AccountItemDelegate.qml</file>
+        <file>src/mainview/components/ConversationListView.qml</file>
+        <file>src/mainview/components/SmartListItemDelegate.qml</file>
+        <file>src/mainview/components/BadgeNotifier.qml</file>
     </qresource>
 </RCC>
diff --git a/src/accountadapter.cpp b/src/accountadapter.cpp
index eefb8c6bc3f04fcb74c9c3b4a29d9ba7ea80a7b3..e29cc2550acf9085bab7b2a1cd79b5f34e23b655 100644
--- a/src/accountadapter.cpp
+++ b/src/accountadapter.cpp
@@ -43,8 +43,6 @@ AccountAdapter::safeInit()
             this,
             &AccountAdapter::onCurrentAccountChanged);
 
-    deselectConversation();
-
     auto accountId = lrcInstance_->getCurrAccId();
     setProperties(accountId);
     connectAccount(accountId);
@@ -65,7 +63,6 @@ AccountAdapter::getDeviceModel()
 void
 AccountAdapter::changeAccount(int row)
 {
-    deselectConversation(); // Hack UI
     auto accountList = lrcInstance_->accountModel().getAccountList();
     if (accountList.size() > row) {
         lrcInstance_->setSelectedAccountId(accountList.at(row));
@@ -269,12 +266,6 @@ AccountAdapter::setCurrAccDisplayName(const QString& text)
     lrcInstance_->setCurrAccDisplayName(text);
 }
 
-void
-AccountAdapter::setSelectedConvId(const QString& convId)
-{
-    lrcInstance_->set_selectedConvUid(convId);
-}
-
 lrc::api::profile::Type
 AccountAdapter::getCurrentAccountType()
 {
@@ -347,23 +338,6 @@ AccountAdapter::passwordSetStatusMessageBox(bool success, QString title, QString
     }
 }
 
-void
-AccountAdapter::deselectConversation()
-{
-    if (lrcInstance_->get_selectedConvUid().isEmpty()) {
-        return;
-    }
-
-    // TODO: remove this unhealthy section
-    auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
-
-    if (currentConversationModel == nullptr) {
-        return;
-    }
-
-    lrcInstance_->set_selectedConvUid();
-}
-
 void
 AccountAdapter::connectAccount(const QString& accountId)
 {
@@ -374,7 +348,7 @@ AccountAdapter::connectAccount(const QString& accountId)
         QObject::disconnect(accountProfileUpdatedConnection_);
         QObject::disconnect(contactAddedConnection_);
         QObject::disconnect(addedToConferenceConnection_);
-        QObject::disconnect(contactUnbannedConnection_);
+        QObject::disconnect(bannedStatusChangedConnection_);
 
         accountStatusChangedConnection_
             = QObject::connect(accInfo.accountModel,
@@ -390,27 +364,22 @@ AccountAdapter::connectAccount(const QString& accountId)
                                    Q_EMIT accountStatusChanged(accountId);
                                });
 
-        contactAddedConnection_
-            = QObject::connect(accInfo.contactModel.get(),
-                               &lrc::api::ContactModel::contactAdded,
-                               [this, accountId](const QString& contactUri) {
-                                   const auto& convInfo = lrcInstance_->getConversationFromConvUid(
-                                       lrcInstance_->get_selectedConvUid());
-                                   if (convInfo.uid.isEmpty()) {
-                                       return;
-                                   }
-                                   auto& accInfo = lrcInstance_->accountModel().getAccountInfo(
-                                       accountId);
-                                   if (contactUri
-                                       == accInfo.contactModel
-                                              ->getContact(convInfo.participants.at(0))
-                                              .profileInfo.uri) {
-                                       /*
-                                        * Update conversation.
-                                        */
-                                       Q_EMIT updateConversationForAddedContact();
-                                   }
-                               });
+        contactAddedConnection_ = QObject::connect(
+            accInfo.contactModel.get(),
+            &lrc::api::ContactModel::contactAdded,
+            [this, accountId](const QString& contactUri) {
+                const auto& convInfo = lrcInstance_->getConversationFromConvUid(
+                    lrcInstance_->get_selectedConvUid());
+                if (convInfo.uid.isEmpty()) {
+                    return;
+                }
+                auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
+                auto selectedContactUri
+                    = accInfo.contactModel->getContact(convInfo.participants.at(0)).profileInfo.uri;
+                if (contactUri == selectedContactUri) {
+                    Q_EMIT selectedContactAdded(convInfo.uid);
+                }
+            });
 
         addedToConferenceConnection_
             = QObject::connect(accInfo.callModel.get(),
@@ -420,12 +389,15 @@ AccountAdapter::connectAccount(const QString& accountId)
                                    lrcInstance_->renderer()->addDistantRenderer(confId);
                                });
 
-        contactUnbannedConnection_ = QObject::connect(accInfo.contactModel.get(),
-                                                      &lrc::api::ContactModel::bannedStatusChanged,
-                                                      [this](const QString&, bool banned) {
-                                                          if (!banned)
-                                                              Q_EMIT contactUnbanned();
-                                                      });
+        bannedStatusChangedConnection_
+            = QObject::connect(accInfo.contactModel.get(),
+                               &lrc::api::ContactModel::bannedStatusChanged,
+                               [this](const QString& uri, bool banned) {
+                                   if (!banned)
+                                       Q_EMIT contactUnbanned();
+                                   else
+                                       Q_EMIT lrcInstance_->contactBanned(uri);
+                               });
     } catch (...) {
         qWarning() << "Couldn't get account: " << accountId;
     }
diff --git a/src/accountadapter.h b/src/accountadapter.h
index 5e11d0509a5579bedf0b9fbf8af08fd944966b53..985781ab36ae5a2c53cbed4d8dd9f5bcd5e562ef 100644
--- a/src/accountadapter.h
+++ b/src/accountadapter.h
@@ -95,16 +95,14 @@ public:
     Q_INVOKABLE bool hasVideoCall();
     Q_INVOKABLE bool isPreviewing();
     Q_INVOKABLE void setCurrAccDisplayName(const QString& text);
-    Q_INVOKABLE void setSelectedConvId(const QString& convId = {});
     Q_INVOKABLE lrc::api::profile::Type getCurrentAccountType();
 
     Q_INVOKABLE void setCurrAccAvatar(bool fromFile, const QString& source);
 
 Q_SIGNALS:
     // Trigger other components to reconnect account related signals.
-    void accountStatusChanged(QString accountId = {});
-
-    void updateConversationForAddedContact();
+    void accountStatusChanged(QString accountId);
+    void selectedContactAdded(QString convId);
 
     // Send report failure to QML to make it show the right UI state .
     void reportFailure();
@@ -119,8 +117,6 @@ private:
     lrc::api::profile::Type currentAccountType_ {};
     int accountListSize_ {};
 
-    void deselectConversation();
-
     // Make account signal connections.
     void connectAccount(const QString& accountId);
 
@@ -134,7 +130,7 @@ private:
     QMetaObject::Connection accountProfileUpdatedConnection_;
     QMetaObject::Connection contactAddedConnection_;
     QMetaObject::Connection addedToConferenceConnection_;
-    QMetaObject::Connection contactUnbannedConnection_;
+    QMetaObject::Connection bannedStatusChangedConnection_;
     QMetaObject::Connection registeredNameSavedConnection_;
 
     AppSettingsManager* settingsManager_;
diff --git a/src/calladapter.cpp b/src/calladapter.cpp
index 58598b72a2039ee560ae2943b75ff41bbbf367da..1be1bec5495ad6b64c6318e37944da99b874ba1a 100644
--- a/src/calladapter.cpp
+++ b/src/calladapter.cpp
@@ -59,9 +59,9 @@ CallAdapter::CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject*
             [this](const QString& accountId, const QString& convUid) {
                 acceptACall(accountId, convUid);
                 Q_EMIT lrcInstance_->notificationClicked();
-                lrcInstance_->selectConversation(accountId, convUid);
+                lrcInstance_->selectConversation(convUid, accountId);
                 updateCall(convUid, accountId);
-                Q_EMIT callSetupMainViewRequired(accountId, convUid);
+                Q_EMIT lrcInstance_->conversationUpdated(convUid, accountId);
             });
     connect(systemTray_,
             &SystemTray::declineCallActivated,
@@ -200,6 +200,7 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
     auto selectedAccountId = lrcInstance_->getCurrAccId();
     auto* callModel = lrcInstance_->getCurrentCallModel();
 
+    // new call
     if (!callModel->hasCall(convInfo.callId)) {
         if (QApplication::focusObject() == nullptr || accountId != selectedAccountId) {
             showNotification(accountId, convInfo.uid);
@@ -219,17 +220,21 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
                 return;
             }
         }
-        Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
-        Q_EMIT lrcInstance_->updateSmartList();
+        // select
+        lrcInstance_->selectConversation(convInfo.uid, accountId);
         return;
     }
 
+    // this slot has been triggered as a result of either selecting a conversation
+    // with an active call, placing a call, or an incoming call for the current
+    // or any other conversation
     auto call = callModel->getCall(convInfo.callId);
     auto isCallSelected = lrcInstance_->get_selectedConvUid() == convInfo.uid;
 
     if (call.isOutgoing) {
         if (isCallSelected) {
-            Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
+            // don't reselect
+            Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
         }
     } else {
         auto accountProperties = lrcInstance_->accountModel().getAccountConfig(selectedAccountId);
@@ -254,10 +259,12 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
                         showNotification(accountId, convInfo.uid);
                         return;
                     } else {
-                        Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
+                        // only update
+                        Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
                     }
                 } else {
-                    Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
+                    // only update
+                    Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
                 }
             } else { // Not current conversation
                 if (currentConvHasCall) {
@@ -269,19 +276,19 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
                         return;
                     }
                 }
-                Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
+                // reselect
+                lrcInstance_->selectConversation(convInfo.uid, accountId);
             }
         }
     }
     Q_EMIT callStatusChanged(static_cast<int>(call.status), accountId, convInfo.uid);
-    Q_EMIT lrcInstance_->updateSmartList();
 }
 
 void
 CallAdapter::onShowCallView(const QString& accountId, const QString& convUid)
 {
     updateCall(convUid, accountId);
-    Q_EMIT callSetupMainViewRequired(accountId, convUid);
+    Q_EMIT lrcInstance_->conversationUpdated(convUid, accountId);
 }
 
 void
@@ -399,8 +406,6 @@ CallAdapter::showNotification(const QString& accountId, const QString& convUid)
             from = accInfo.contactModel->bestNameForContact(convInfo.participants[0]);
     }
 
-    Q_EMIT lrcInstance_->updateSmartList();
-
 #ifdef Q_OS_LINUX
     auto contactPhoto = Utils::contactPhoto(lrcInstance_,
                                             convInfo.participants[0],
@@ -414,12 +419,11 @@ CallAdapter::showNotification(const QString& accountId, const QString& convUid)
                                   Utils::QImageToByteArray(contactPhoto));
 #else
     auto onClicked = [this, accountId, convUid = convInfo.uid]() {
+        Q_EMIT lrcInstance_->notificationClicked();
         const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId);
-        if (convInfo.uid.isEmpty()) {
+        if (convInfo.uid.isEmpty())
             return;
-        }
-        Q_EMIT lrcInstance_->notificationClicked();
-        Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
+        lrcInstance_->selectConversation(convInfo.uid, accountId);
     };
     systemTray_->showNotification(tr("is calling you"), from, onClicked);
 #endif
@@ -484,7 +488,7 @@ CallAdapter::connectCallModel(const QString& accountId)
             case lrc::api::call::Status::TIMEOUT:
             case lrc::api::call::Status::TERMINATING: {
                 lrcInstance_->renderer()->removeDistantRenderer(callId);
-                Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
+                Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
                 if (convInfo.uid.isEmpty()) {
                     break;
                 }
@@ -517,7 +521,7 @@ CallAdapter::connectCallModel(const QString& accountId)
                                 /*
                                  * Reset the call view corresponding accountId, uid.
                                  */
-                                lrcInstance_->set_selectedConvUid(otherConv.uid);
+                                lrcInstance_->selectConversation(otherConv.uid);
                                 updateCall(otherConv.uid, otherConv.accountId, forceCallOnly);
                             }
                         }
diff --git a/src/calladapter.h b/src/calladapter.h
index a93fafa188079ab956d90e6352af07eda9ed7d61..efb9ddccc8a0d23ca544ee74536978f9b5e8b36e 100644
--- a/src/calladapter.h
+++ b/src/calladapter.h
@@ -80,11 +80,9 @@ public:
 
 Q_SIGNALS:
     void callStatusChanged(int index, const QString& accountId, const QString& convUid);
-    void updateConversationSmartList();
     void updateParticipantsInfos(const QVariantList& infos,
                                  const QString& accountId,
                                  const QString& callId);
-    void callSetupMainViewRequired(const QString& accountId, const QString& convUid);
     void previewVisibilityNeedToChange(bool visible);
 
     // For Call Overlay
diff --git a/src/commoncomponents/AvatarImage.qml b/src/commoncomponents/AvatarImage.qml
index 06a0edd269451041dfb662a3acda191437770f1d..96acf5ebc0750ca42fde5500a5cb88edd2b3f945 100644
--- a/src/commoncomponents/AvatarImage.qml
+++ b/src/commoncomponents/AvatarImage.qml
@@ -38,6 +38,7 @@ Item {
 
     property alias fillMode: rootImage.fillMode
     property alias sourceSize: rootImage.sourceSize
+    property int transitionDuration: 150
     property bool saveToConfig: false
     property int mode: AvatarImage.Mode.FromAccount
     property string imageProviderIdPrefix: {
@@ -178,7 +179,7 @@ Item {
                 NumberAnimation {
                     properties: "opacity"
                     easing.type: Easing.InOutQuad
-                    duration: 400
+                    duration: transitionDuration
                 }
             }
         }
@@ -188,38 +189,15 @@ Item {
         id: presenceIndicator
 
         anchors.right: root.right
+        anchors.rightMargin: -1
         anchors.bottom: root.bottom
+        anchors.bottomMargin: -1
 
-        size: root.width * 0.3
+        size: root.width * 0.26
 
         visible: showPresenceIndicator
     }
 
-    Rectangle {
-        id: unreadMessageCountRect
-
-        anchors.right: root.right
-        anchors.top: root.top
-
-        width: root.width * 0.3
-        height: root.width * 0.3
-
-        visible: unreadMessagesCount > 0
-
-        Text {
-            id: unreadMessageCounttext
-
-            anchors.centerIn: unreadMessageCountRect
-
-            text: unreadMessagesCount > 9 ? "…" : unreadMessagesCount
-            color: "white"
-            font.pointSize: JamiTheme.indicatorFontSize
-        }
-
-        radius: 30
-        color: JamiTheme.notificationRed
-    }
-
     Connections {
         target: ScreenInfo
 
diff --git a/src/commoncomponents/BaseContextMenu.qml b/src/commoncomponents/BaseContextMenu.qml
index da0b96397b3f6d3030833169b4a2b94ce10ee4ad..636fc74e2b2ae765ecda7db6a2ef198436d96aa5 100644
--- a/src/commoncomponents/BaseContextMenu.qml
+++ b/src/commoncomponents/BaseContextMenu.qml
@@ -31,6 +31,12 @@ Menu {
     property int commonBorderWidth: 1
     font.pointSize: JamiTheme.menuFontSize
 
+    modal: true
+    Overlay.modal: Rectangle {
+        color: "transparent"
+    }
+
+    // TODO: investigate
     function openMenu(){
         visible = true
         visible = false
@@ -38,6 +44,8 @@ Menu {
     }
 
     background: Rectangle {
+        id: container
+
         implicitWidth: menuItemsPreferredWidth
         implicitHeight: menuItemsPreferredHeight
                         * (root.count - generalMenuSeparatorCount)
@@ -45,5 +53,15 @@ Menu {
         border.width: commonBorderWidth
         border.color: JamiTheme.tabbarBorderColor
         color: JamiTheme.backgroundColor
+
+        layer.enabled: true
+        layer.effect: DropShadow {
+            z: -1
+            horizontalOffset: 3.0
+            verticalOffset: 3.0
+            radius: 16.0
+            samples: 16
+            color: JamiTheme.shadowColor
+        }
     }
 }
diff --git a/src/commoncomponents/ModalPopup.qml b/src/commoncomponents/ModalPopup.qml
index 95a3ae48d9d8dbddf955fdd70f1fbf2c9ca32ccb..83e7d8201ca0f2819fdd0f2c45af963c2a905bd9 100644
--- a/src/commoncomponents/ModalPopup.qml
+++ b/src/commoncomponents/ModalPopup.qml
@@ -67,7 +67,7 @@ Popup {
         height: root.height
         horizontalOffset: 3.0
         verticalOffset: 3.0
-        radius: container.radius * 2
+        radius: container.radius * 4
         samples: 16
         color: JamiTheme.shadowColor
         source: container
diff --git a/src/commoncomponents/PresenceIndicator.qml b/src/commoncomponents/PresenceIndicator.qml
index d7baf7692d0b1031c30af8c93504df544c57ff66..5ee5825abef9cfb5a7ab01364365a26d3413e568 100644
--- a/src/commoncomponents/PresenceIndicator.qml
+++ b/src/commoncomponents/PresenceIndicator.qml
@@ -30,7 +30,7 @@ Rectangle {
     // This is set to REGISTERED for contact presence
     // as status is not currently tracked for contact items.
     property int status: Account.Status.REGISTERED
-    property int size: 12
+    property int size: 15
 
     width: size
     height: size
diff --git a/src/commoncomponents/Scaffold.qml b/src/commoncomponents/Scaffold.qml
index 9acb5a0991c889f32a74d272f397a482c7458081..4e76288c78be0f904058fc8fc456c725650e9bd8 100644
--- a/src/commoncomponents/Scaffold.qml
+++ b/src/commoncomponents/Scaffold.qml
@@ -23,6 +23,12 @@ import QtQuick.Controls 2.12
 Rectangle {
     property alias name: label.text
     property bool stretchParent: false
+    property string tag: this.toString()
+    signal moveX(real dx)
+    signal moveY(real dy)
+    property real ox: 0
+    property real oy: 0
+    property real step: 0.5
 
     border.width: 1
     color: {
@@ -33,6 +39,19 @@ Rectangle {
     }
     anchors.fill: parent
     focus: false
+    Keys.onPressed: {
+        if (event.key === Qt.Key_Left)
+            moveX(-step)
+        else if (event.key === Qt.Key_Right)
+            moveX(step)
+        else if (event.key === Qt.Key_Down)
+            moveY(step)
+        else if (event.key === Qt.Key_Up)
+            moveY(-step)
+        console.log(tag, ox, oy)
+        event.accepted = true;
+    }
+
     Component.onCompleted: {
         // fallback to some description of the object
         if (label.text === "")
@@ -45,10 +64,24 @@ Rectangle {
         }
     }
 
+    onMoveX: {
+        parent.anchors.leftMargin += dx
+        parent.x += dx
+        ox += dx;
+    }
+    onMoveY: {
+        parent.anchors.topMargin += dy
+        parent.y += dy
+        oy += dy
+    }
+
     Label {
         id: label
-
         anchors.centerIn: parent
     }
 
+    MouseArea {
+        anchors.fill: parent
+        onPressed: parent.forceActiveFocus()
+    }
 }
diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml
index f1b7c5d890e57e81c12e25031c7c89c8fd1f3bb7..fdf9b441948d1979885f8f9f0dd891ea163523b7 100644
--- a/src/constant/JamiStrings.qml
+++ b/src/constant/JamiStrings.qml
@@ -330,9 +330,6 @@ Item {
                                 "Use the \"Link Another Device\" feature to obtain a PIN.")
     property string connectFromAnotherDevice: qsTr("Link device")
 
-    // KeyBoardShortcutTable
-    property string conversations: qsTr("Conversations")
-
     // LinkDevicesDialog
     property string pinTimerInfos: qsTr("The PIN and the account password should be entered in your device within 10 minutes.")
     property string close: qsTr("Close")
@@ -405,6 +402,8 @@ Item {
 
     // SmartList
     property string clearText: qsTr("Clear Text")
+    property string conversations: qsTr("Conversations")
+    property string searchResults: qsTr("Search Results")
 
     // SmartList context menu
     property string declineContactRequest: qsTr("Decline contact request")
diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml
index d7b5db5f9bd2f4e014c9d972feb2d8d7c995a9e0..43df8965d2ce9381f965c4843a7ea9ae8b1944c6 100644
--- a/src/constant/JamiTheme.qml
+++ b/src/constant/JamiTheme.qml
@@ -55,10 +55,10 @@ Item {
     property color notificationBlue: "#31b7ff"
     property color unPresenceOrange: "orange"
     property color placeHolderTextFontColor: "#767676"
-    property color draftRed: "#cf5300"
+    property color draftTextColor: "#cf5300"
     property color selectedTabColor: primaryForegroundColor
-    property color filterBadgeColor: mediumGrey
-    property color filterBadgeTextColor: blackColor
+    property color filterBadgeColor: "#eed4d8"
+    property color filterBadgeTextColor: "#cc0022"
 
     // General buttons
     property color pressedButtonColor: darkTheme ? pressColor : "#a0a0a0"
@@ -174,10 +174,14 @@ Item {
     property real titleFontSize: 16
     property real primaryRadius: 4
     property real smartlistItemFontSize: 10.5
+    property real smartlistItemInfoFontSize: 9
     property real filterItemFontSize: smartlistItemFontSize
     property real filterBadgeFontSize: 8.25
     property real accountListItemHeight: 64
     property real accountListAvatarSize: 40
+    property real smartListItemHeight: 64
+    property real smartListAvatarSize: 52
+    property real smartListTransitionDuration: 120
 
     property real maximumWidthSettingsView: 600
     property real settingsHeaderpreferredHeight: 64
diff --git a/src/contactadapter.cpp b/src/contactadapter.cpp
index d35cc64f2b522b037bc9099df7d07a4405c0b2a9..b4b8a4392e8594c737e9d55a3c6ab4396b785d86 100644
--- a/src/contactadapter.cpp
+++ b/src/contactadapter.cpp
@@ -1,4 +1,4 @@
-/*!
+/*
  * Copyright (C) 2020 by Savoir-faire Linux
  * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
@@ -48,13 +48,13 @@ ContactAdapter::getContactSelectableModel(int type)
     switch (listModeltype_) {
     case SmartListModel::Type::CONVERSATION:
         selectableProxyModel_->setPredicate([this](const QModelIndex& index, const QRegExp&) {
-            return !defaultModerators_.contains(index.data(SmartListModel::URI).toString());
+            return !defaultModerators_.contains(index.data(Role::URI).toString());
         });
         break;
 
     case SmartListModel::Type::CONFERENCE:
         selectableProxyModel_->setPredicate([](const QModelIndex& index, const QRegExp&) {
-            return index.data(SmartListModel::Presence).toBool();
+            return index.data(Role::Presence).toBool();
         });
         break;
     case SmartListModel::Type::TRANSFER:
@@ -68,13 +68,11 @@ ContactAdapter::getContactSelectableModel(int type)
                                               .contactModel->bestIdForContact(conv.participants[0]);
 
                 QRegExp matchExcept = QRegExp(QString("\\b(?!" + calleeDisplayId + "\\b)\\w+"));
-                match = matchExcept.indexIn(index.data(SmartListModel::Role::DisplayID).toString())
-                        != -1;
+                match = matchExcept.indexIn(index.data(Role::BestId).toString()) != -1;
             }
 
             if (match) {
-                match = regexp.indexIn(index.data(SmartListModel::Role::DisplayID).toString())
-                        != -1;
+                match = regexp.indexIn(index.data(Role::BestId).toString()) != -1;
             }
             return match && !index.parent().isValid();
         });
@@ -95,8 +93,8 @@ ContactAdapter::setSearchFilter(const QString& filter)
     } else if (listModeltype_ == SmartListModel::Type::CONVERSATION) {
         selectableProxyModel_->setPredicate(
             [this, filter](const QModelIndex& index, const QRegExp&) {
-                return (!defaultModerators_.contains(index.data(SmartListModel::URI).toString())
-                        && index.data(SmartListModel::DisplayName).toString().contains(filter));
+                return (!defaultModerators_.contains(index.data(Role::URI).toString())
+                        && index.data(Role::BestName).toString().contains(filter));
             });
     }
     selectableProxyModel_->setFilterRegExp(
@@ -114,15 +112,14 @@ ContactAdapter::contactSelected(int index)
         switch (listModeltype_) {
         case SmartListModel::Type::CONFERENCE: {
             // Conference.
-            const auto sectionName = contactIndex.data(SmartListModel::Role::SectionName)
-                                         .value<QString>();
+            const auto sectionName = contactIndex.data(Role::SectionName).value<QString>();
             if (!sectionName.isEmpty()) {
                 smartListModel_->toggleSection(sectionName);
                 return;
             }
 
-            const auto convUid = contactIndex.data(SmartListModel::Role::UID).value<QString>();
-            const auto accId = contactIndex.data(SmartListModel::Role::AccountId).value<QString>();
+            const auto convUid = contactIndex.data(Role::UID).value<QString>();
+            const auto accId = contactIndex.data(Role::AccountId).value<QString>();
             const auto callId = lrcInstance_->getCallIdForConversationUid(convUid, accId);
 
             if (!callId.isEmpty()) {
@@ -133,7 +130,7 @@ ContactAdapter::contactSelected(int index)
 
                 callModel->joinCalls(thisCallId, callId);
             } else {
-                const auto contactUri = contactIndex.data(SmartListModel::Role::URI).value<QString>();
+                const auto contactUri = contactIndex.data(Role::URI).value<QString>();
                 auto call = lrcInstance_->getCallInfoForConversation(convInfo);
                 if (!call) {
                     return;
@@ -143,7 +140,7 @@ ContactAdapter::contactSelected(int index)
         } break;
         case SmartListModel::Type::TRANSFER: {
             // SIP Transfer.
-            const auto contactUri = contactIndex.data(SmartListModel::Role::URI).value<QString>();
+            const auto contactUri = contactIndex.data(Role::URI).value<QString>();
 
             if (convInfo.uid.isEmpty()) {
                 return;
@@ -170,7 +167,7 @@ ContactAdapter::contactSelected(int index)
             }
         } break;
         case SmartListModel::Type::CONVERSATION: {
-            const auto contactUri = contactIndex.data(SmartListModel::Role::URI).value<QString>();
+            const auto contactUri = contactIndex.data(Role::URI).value<QString>();
             if (contactUri.isEmpty()) {
                 return;
             }
diff --git a/src/contactadapter.h b/src/contactadapter.h
index f5f30ea657c1b162154ebcf4c27efa8e19d2c1f8..bb958c0036943309d5b98aeba8d9d1ac7eb8dfec 100644
--- a/src/contactadapter.h
+++ b/src/contactadapter.h
@@ -1,4 +1,4 @@
-/*!
+/*
  * Copyright (C) 2020 by Savoir-faire Linux
  * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
  *
@@ -20,6 +20,7 @@
 
 #include "qmladapterbase.h"
 #include "smartlistmodel.h"
+#include "conversationlistmodel.h"
 
 #include <QObject>
 #include <QSortFilterProxyModel>
@@ -38,30 +39,42 @@ class LRCInstance;
  */
 class SelectableProxyModel final : public QSortFilterProxyModel
 {
+    Q_OBJECT
+
 public:
     using FilterPredicate = std::function<bool(const QModelIndex&, const QRegExp&)>;
 
-    explicit SelectableProxyModel(QAbstractItemModel* parent)
+    explicit SelectableProxyModel(QAbstractListModel* parent = nullptr)
         : QSortFilterProxyModel(parent)
     {
         setSourceModel(parent);
+        setSortRole(ConversationList::Role::LastInteractionTimeStamp);
+        sort(0, Qt::DescendingOrder);
+        setFilterCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive);
     }
-    ~SelectableProxyModel() {}
 
     void setPredicate(FilterPredicate filterPredicate)
     {
         filterPredicate_ = filterPredicate;
     }
 
-    virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
+    virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
     {
         // Accept all contacts in conversation list filtered with account type, except those in a call.
-        auto index = sourceModel()->index(source_row, 0, source_parent);
+        auto index = sourceModel()->index(sourceRow, 0, sourceParent);
         return filterPredicate_ ? filterPredicate_(index, filterRegExp()) : false;
     }
 
+    bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
+    {
+        QVariant leftData = sourceModel()->data(left, sortRole());
+        QVariant rightData = sourceModel()->data(right, sortRole());
+        // we're assuming the sort role data type here is some integral time
+        return leftData.toUInt() < rightData.toUInt();
+    };
+
 private:
-    std::function<bool(const QModelIndex&, const QRegExp&)> filterPredicate_;
+    FilterPredicate filterPredicate_;
 };
 
 class ContactAdapter final : public QmlAdapterBase
@@ -73,6 +86,8 @@ public:
     ~ContactAdapter() = default;
 
 protected:
+    using Role = ConversationList::Role;
+
     void safeInit() override {};
 
     Q_INVOKABLE QVariant getContactSelectableModel(int type);
@@ -81,10 +96,8 @@ protected:
 
 private:
     SmartListModel::Type listModeltype_;
-
-    // SmartListModel is the source model of SelectableProxyModel.
-    std::unique_ptr<SmartListModel> smartListModel_;
-    std::unique_ptr<SelectableProxyModel> selectableProxyModel_;
+    QScopedPointer<SmartListModel> smartListModel_;
+    QScopedPointer<SelectableProxyModel> selectableProxyModel_;
 
     QStringList defaultModerators_;
 
diff --git a/src/conversationlistmodel.cpp b/src/conversationlistmodel.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cd3fd035a540207c242da23d0cd7e590f29df664
--- /dev/null
+++ b/src/conversationlistmodel.cpp
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ * Author: Mingrui Zhang <mingrui.zhang@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 "conversationlistmodel.h"
+
+#include "uri.h"
+
+ConversationListModel::ConversationListModel(LRCInstance* instance, QObject* parent)
+    : ConversationListModelBase(instance, parent)
+{
+    connect(
+        model_,
+        &ConversationModel::beginInsertRows,
+        this,
+        [this](int position, int rows) {
+            beginInsertRows(QModelIndex(), position, position + (rows - 1));
+        },
+        Qt::DirectConnection);
+    connect(model_,
+            &ConversationModel::endInsertRows,
+            this,
+            &ConversationListModel::endInsertRows,
+            Qt::DirectConnection);
+
+    connect(
+        model_,
+        &ConversationModel::beginRemoveRows,
+        this,
+        [this](int position, int rows) {
+            beginRemoveRows(QModelIndex(), position, position + (rows - 1));
+        },
+        Qt::DirectConnection);
+    connect(model_,
+            &ConversationModel::endRemoveRows,
+            this,
+            &ConversationListModel::endRemoveRows,
+            Qt::DirectConnection);
+
+    connect(model_, &ConversationModel::dataChanged, this, [this](int position) {
+        const auto index = createIndex(position, 0);
+        Q_EMIT ConversationListModel::dataChanged(index, index);
+    });
+}
+
+int
+ConversationListModel::rowCount(const QModelIndex& parent) const
+{
+    // For list models only the root node (an invalid parent) should return the list's size. For all
+    // other (valid) parents, rowCount() should return 0 so that it does not become a tree model.
+    if (!parent.isValid() && model_) {
+        return model_->getConversations().size();
+    }
+    return 0;
+}
+
+QVariant
+ConversationListModel::data(const QModelIndex& index, int role) const
+{
+    const auto& data = model_->getConversations();
+    if (!index.isValid() || data.empty())
+        return {};
+    return dataForItem(data.at(index.row()), role);
+}
+
+ConversationListProxyModel::ConversationListProxyModel(QAbstractListModel* model, QObject* parent)
+    : SelectableListProxyModel(model, parent)
+{
+    setSortRole(ConversationList::Role::LastInteractionTimeStamp);
+    sort(0, Qt::DescendingOrder);
+    setFilterCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive);
+}
+
+bool
+ConversationListProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
+{
+    QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
+    auto rx = filterRegExp();
+    auto uriStripper = URI(rx.pattern());
+    bool stripScheme = (uriStripper.schemeType() < URI::SchemeType::COUNT__);
+    FlagPack<URI::Section> flags = URI::Section::USER_INFO | URI::Section::HOSTNAME
+                                   | URI::Section::PORT;
+    if (!stripScheme) {
+        flags |= URI::Section::SCHEME;
+    }
+    rx.setPattern(uriStripper.format(flags));
+    auto uri = index.data(ConversationList::Role::URI).toString();
+    auto alias = index.data(ConversationList::Role::Alias).toString();
+    auto registeredName = index.data(ConversationList::Role::RegisteredName).toString();
+    auto itemProfileType = index.data(ConversationList::Role::ContactType).toInt();
+    auto typeFilter = static_cast<profile::Type>(itemProfileType) == currentTypeFilter_;
+    if (index.data(ConversationList::Role::IsBanned).toBool()) {
+        return typeFilter
+               && (rx.exactMatch(uri) || rx.exactMatch(alias) || rx.exactMatch(registeredName));
+    }
+    return typeFilter
+           && (rx.indexIn(uri) != -1 || rx.indexIn(alias) != -1 || rx.indexIn(registeredName) != -1);
+}
+
+bool
+ConversationListProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
+{
+    QVariant leftData = sourceModel()->data(left, sortRole());
+    QVariant rightData = sourceModel()->data(right, sortRole());
+    // we're assuming the sort role data type here is some integral time
+    return leftData.toULongLong() < rightData.toULongLong();
+}
+
+void
+ConversationListProxyModel::setTypeFilter(const profile::Type& typeFilter)
+{
+    beginResetModel();
+    currentTypeFilter_ = typeFilter;
+    endResetModel();
+    updateSelection();
+};
diff --git a/src/conversationlistmodel.h b/src/conversationlistmodel.h
new file mode 100644
index 0000000000000000000000000000000000000000..3e143d740fb79b7e71c9f35081accc2bb44d7901
--- /dev/null
+++ b/src/conversationlistmodel.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ * Author: Mingrui Zhang <mingrui.zhang@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/>.
+ */
+
+#pragma once
+
+#include "conversationlistmodelbase.h"
+#include "selectablelistproxymodel.h"
+
+#include "api/profile.h"
+
+#include <QSortFilterProxyModel>
+
+// A wrapper view model around ConversationModel's underlying data
+class ConversationListModel final : public ConversationListModelBase
+{
+    Q_OBJECT
+
+public:
+    explicit ConversationListModel(LRCInstance* instance, QObject* parent = nullptr);
+
+    int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+    virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+};
+
+// The top level filtered and sorted model to be consumed by QML ListViews
+class ConversationListProxyModel final : public SelectableListProxyModel
+{
+    Q_OBJECT
+
+public:
+    explicit ConversationListProxyModel(QAbstractListModel* model, QObject* parent = nullptr);
+    bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
+    bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
+
+    Q_INVOKABLE void setTypeFilter(const profile::Type& typeFilter);
+
+private:
+    profile::Type currentTypeFilter_;
+};
diff --git a/src/conversationlistmodelbase.cpp b/src/conversationlistmodelbase.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f87f5c9d5f6752842902b1fb2ac7d82e570fe707
--- /dev/null
+++ b/src/conversationlistmodelbase.cpp
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2020-2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ * Author: Mingrui Zhang <mingrui.zhang@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 "conversationlistmodelbase.h"
+
+ConversationListModelBase::ConversationListModelBase(LRCInstance* instance, QObject* parent)
+    : AbstractListModelBase(parent)
+{
+    lrcInstance_ = instance;
+    model_ = lrcInstance_->getCurrentConversationModel();
+}
+
+int
+ConversationListModelBase::columnCount(const QModelIndex& parent) const
+{
+    Q_UNUSED(parent)
+    return 1;
+}
+
+QHash<int, QByteArray>
+ConversationListModelBase::roleNames() const
+{
+    using namespace ConversationList;
+    QHash<int, QByteArray> roles;
+#define X(role) roles[role] = #role;
+    CONV_ROLES
+#undef X
+    return roles;
+}
+
+QVariant
+ConversationListModelBase::dataForItem(item_t item, int role) const
+{
+    if (item.participants.isEmpty()) {
+        return QVariant();
+    }
+    // WARNING: not swarm ready
+    auto peerUri = item.participants[0];
+    ContactModel* contactModel {nullptr};
+    contact::Info contact {};
+    try {
+        const auto& accountInfo = lrcInstance_->getAccountInfo(item.accountId);
+        contactModel = accountInfo.contactModel.get();
+        contact = contactModel->getContact(peerUri);
+    } catch (...) {
+        return QVariant(false);
+    }
+
+    // Since we are using image provider right now, image url representation should be unique to
+    // be able to use the image cache, account avatar will only be updated once PictureUid changed
+    switch (role) {
+    case Role::BestName:
+        return QVariant(contactModel->bestNameForContact(peerUri));
+    case Role::BestId:
+        return QVariant(contactModel->bestIdForContact(peerUri));
+    case Role::Presence:
+        return QVariant(contact.isPresent);
+    case Role::PictureUid:
+        return QVariant(contactAvatarUidMap_[peerUri]);
+    case Role::Alias:
+        return QVariant(contact.profileInfo.alias);
+    case Role::RegisteredName:
+        return QVariant(contact.registeredName);
+    case Role::URI:
+        return QVariant(peerUri);
+    case Role::UnreadMessagesCount:
+        return QVariant(item.unreadMessages);
+    case Role::LastInteractionTimeStamp: {
+        if (!item.interactions.empty()) {
+            auto ts = static_cast<qint32>(item.interactions.at(item.lastMessageUid).timestamp);
+            return QVariant(ts);
+        }
+        break;
+    }
+    case Role::LastInteractionDate: {
+        if (!item.interactions.empty()) {
+            auto& date = item.interactions.at(item.lastMessageUid).timestamp;
+            return QVariant(Utils::formatTimeString(date));
+        }
+        break;
+    }
+    case Role::LastInteraction: {
+        if (!item.interactions.empty()) {
+            return QVariant(item.interactions.at(item.lastMessageUid).body);
+        }
+        break;
+    }
+    case Role::ContactType: {
+        return QVariant(static_cast<int>(contact.profileInfo.type));
+    }
+    case Role::IsBanned: {
+        return QVariant(contact.isBanned);
+    }
+    case Role::UID:
+        return QVariant(item.uid);
+    case Role::InCall: {
+        const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
+        if (!convInfo.uid.isEmpty()) {
+            auto* callModel = lrcInstance_->getCurrentCallModel();
+            return QVariant(callModel->hasCall(convInfo.callId));
+        }
+        return QVariant(false);
+    }
+    case Role::IsAudioOnly: {
+        const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
+        if (!convInfo.uid.isEmpty()) {
+            auto* call = lrcInstance_->getCallInfoForConversation(convInfo);
+            if (call) {
+                return QVariant(call->isAudioOnly);
+            }
+        }
+        return QVariant();
+    }
+    case Role::CallStackViewShouldShow: {
+        const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
+        if (!convInfo.uid.isEmpty() && !convInfo.callId.isEmpty()) {
+            auto* callModel = lrcInstance_->getCurrentCallModel();
+            const auto& call = callModel->getCall(convInfo.callId);
+            return QVariant(callModel->hasCall(convInfo.callId)
+                            && ((!call.isOutgoing
+                                 && (call.status == call::Status::IN_PROGRESS
+                                     || call.status == call::Status::PAUSED
+                                     || call.status == call::Status::INCOMING_RINGING))
+                                || (call.isOutgoing && call.status != call::Status::ENDED)));
+        }
+        return QVariant(false);
+    }
+    case Role::CallState: {
+        const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
+        if (!convInfo.uid.isEmpty()) {
+            if (auto* call = lrcInstance_->getCallInfoForConversation(convInfo)) {
+                return QVariant(static_cast<int>(call->status));
+            }
+        }
+        return QVariant();
+    }
+    case Role::Draft: {
+        if (!item.uid.isEmpty()) {
+            const auto draft = lrcInstance_->getContentDraft(item.uid, item.accountId);
+            if (!draft.isEmpty()) {
+                // Pencil Emoji
+                uint cp = 0x270F;
+                auto emojiString = QString::fromUcs4(&cp, 1);
+                return emojiString + draft;
+            }
+        }
+        return QVariant("");
+    }
+    }
+    return QVariant();
+}
+
+void
+ConversationListModelBase::updateContactAvatarUid(const QString& contactUri)
+{
+    contactAvatarUidMap_[contactUri] = Utils::generateUid();
+}
+
+void
+ConversationListModelBase::fillContactAvatarUidMap(
+    const lrc::api::ContactModel::ContactInfoMap& contacts)
+{
+    if (contacts.size() == 0) {
+        contactAvatarUidMap_.clear();
+        return;
+    }
+
+    if (contactAvatarUidMap_.isEmpty() || contacts.size() != contactAvatarUidMap_.size()) {
+        bool useContacts = contacts.size() > contactAvatarUidMap_.size();
+        auto contactsKeyList = contacts.keys();
+        auto contactAvatarUidMapKeyList = contactAvatarUidMap_.keys();
+
+        for (int i = 0;
+             i < (useContacts ? contactsKeyList.size() : contactAvatarUidMapKeyList.size());
+             ++i) {
+            // Insert or update
+            if (i < contactsKeyList.size() && !contactAvatarUidMap_.contains(contactsKeyList.at(i)))
+                contactAvatarUidMap_.insert(contactsKeyList.at(i), Utils::generateUid());
+            // Remove
+            if (i < contactAvatarUidMapKeyList.size()
+                && !contacts.contains(contactAvatarUidMapKeyList.at(i)))
+                contactAvatarUidMap_.remove(contactAvatarUidMapKeyList.at(i));
+        }
+    }
+}
diff --git a/src/conversationlistmodelbase.h b/src/conversationlistmodelbase.h
new file mode 100644
index 0000000000000000000000000000000000000000..fe99fdd0402192e454a0ae1030c60a0c36695491
--- /dev/null
+++ b/src/conversationlistmodelbase.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020-2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ * Author: Mingrui Zhang <mingrui.zhang@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/>.
+ */
+
+#pragma once
+
+#include "abstractlistmodelbase.h"
+
+// TODO: many of these roles should probably be factored out
+#define CONV_ROLES \
+    X(BestName) \
+    X(BestId) \
+    X(Presence) \
+    X(Alias) \
+    X(RegisteredName) \
+    X(URI) \
+    X(UnreadMessagesCount) \
+    X(LastInteractionTimeStamp) \
+    X(LastInteractionDate) \
+    X(LastInteraction) \
+    X(ContactType) \
+    X(IsBanned) \
+    X(UID) \
+    X(InCall) \
+    X(IsAudioOnly) \
+    X(CallStackViewShouldShow) \
+    X(CallState) \
+    X(SectionName) \
+    X(AccountId) \
+    X(PictureUid) \
+    X(Draft)
+
+namespace ConversationList {
+Q_NAMESPACE
+enum Role {
+    DummyRole = Qt::UserRole + 1,
+#define X(role) role,
+    CONV_ROLES
+#undef X
+};
+Q_ENUM_NS(Role)
+} // namespace ConversationList
+
+// A generic wrapper view model around ConversationModel's underlying data
+class ConversationListModelBase : public AbstractListModelBase
+{
+    Q_OBJECT
+
+public:
+    using item_t = const conversation::Info&;
+
+    explicit ConversationListModelBase(LRCInstance* instance, QObject* parent = nullptr);
+
+    int columnCount(const QModelIndex& parent) const override;
+    QHash<int, QByteArray> roleNames() const override;
+
+    QVariant dataForItem(item_t item, int role = Qt::DisplayRole) const;
+
+    // Update the avatar uid map to prevent the image provider from pulling from the cache
+    void updateContactAvatarUid(const QString& contactUri);
+
+protected:
+    using Role = ConversationList::Role;
+
+    // Assign a uid for each contact avatar; it will serve as the PictureUid role
+    void fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts);
+
+    // Convenience pointer to be pulled from lrcinstance
+    ConversationModel* model_;
+
+    // AvatarImageProvider helper
+    QMap<QString, QString> contactAvatarUidMap_;
+};
diff --git a/src/conversationsadapter.cpp b/src/conversationsadapter.cpp
index 6b7b76ba49db4b1094f91f3010809200f762f944..ade05c5975386ee11e241d04a94996f8db4eeecd 100644
--- a/src/conversationsadapter.cpp
+++ b/src/conversationsadapter.cpp
@@ -1,11 +1,7 @@
-/*!
+/*
  * Copyright (C) 2020 by Savoir-faire Linux
- * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
- * Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
- * Author: Olivier Soldano <olivier.soldano@savoirfairelinux.com>
- * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
- * Author: Isa Nanic <isa.nanic@savoirfairelinux.com>
  * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@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
@@ -26,35 +22,92 @@
 #include "utils.h"
 #include "qtutils.h"
 #include "systemtray.h"
+#include "qmlregister.h"
 
 #include <QApplication>
+#include <QJsonObject>
+
+using namespace lrc::api;
 
 ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
                                            LRCInstance* instance,
                                            QObject* parent)
     : QmlAdapterBase(instance, parent)
+    , currentTypeFilter_(profile::Type::RING)
     , systemTray_(systemTray)
+    , convSrcModel_(new ConversationListModel(lrcInstance_))
+    , convModel_(new ConversationListProxyModel(convSrcModel_.get()))
+    , searchSrcModel_(new SearchResultsListModel(lrcInstance_))
+    , searchModel_(new SelectableListProxyModel(searchSrcModel_.get()))
 {
+    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, convModel_.get(), "ConversationListModel");
+    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, searchModel_.get(), "SearchResultsListModel");
+
+    new SelectableListProxyGroupModel({convModel_.data(), searchModel_.data()}, this);
+
+    setTypeFilter(currentTypeFilter_);
     connect(this, &ConversationsAdapter::currentTypeFilterChanged, [this]() {
-        lrcInstance_->getCurrentConversationModel()->setFilter(currentTypeFilter_);
+        setTypeFilter(currentTypeFilter_);
     });
 
-    connect(lrcInstance_, &LRCInstance::conversationSelected, [this]() {
-        auto convUid = lrcInstance_->get_selectedConvUid();
-        if (!convUid.isEmpty()) {
-            Q_EMIT showConversation(lrcInstance_->getCurrAccId(), convUid);
+    connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() {
+        auto convId = lrcInstance_->get_selectedConvUid();
+        if (convId.isEmpty()) {
+            // deselected
+            convModel_->deselect();
+            searchModel_->deselect();
+        } else {
+            // selected
+            const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
+            if (convInfo.uid.isEmpty())
+                return;
+
+            auto& accInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
+            accInfo.conversationModel->selectConversation(convInfo.uid);
+            accInfo.conversationModel->clearUnreadInteractions(convInfo.uid);
+
+            try {
+                // Set contact filter (for conversation tab selection)
+                // WARNING: not swarm ready
+                auto& contact = accInfo.contactModel->getContact(convInfo.participants.front());
+                if (contact.profileInfo.type != profile::Type::INVALID
+                    && contact.profileInfo.type != profile::Type::TEMPORARY)
+                    set_currentTypeFilter(contact.profileInfo.type);
+            } catch (const std::out_of_range& e) {
+                qWarning() << e.what();
+            }
+
+            // reposition index in case of programmatic selection
+            // currently, this may only occur for the conversation list
+            // and not the search list
+            convModel_->selectSourceRow(lrcInstance_->indexOf(convId));
         }
     });
 
+    connect(lrcInstance_, &LRCInstance::draftSaved, [this](const QString& convId) {
+        auto row = lrcInstance_->indexOf(convId);
+        const auto index = convSrcModel_->index(row, 0);
+        Q_EMIT convSrcModel_->dataChanged(index, index);
+    });
+
+    connect(lrcInstance_, &LRCInstance::contactBanned, [this](const QString& uri) {
+        auto& convInfo = lrcInstance_->getConversationFromPeerUri(uri);
+        if (convInfo.uid.isEmpty())
+            return;
+        auto row = lrcInstance_->indexOf(convInfo.uid);
+        const auto index = convSrcModel_->index(row, 0);
+        Q_EMIT convSrcModel_->dataChanged(index, index);
+    });
+
+    updateConversationFilterData();
+
 #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);
+                lrcInstance_->selectConversation(convUid, accountId);
             });
     connect(systemTray_,
             &SystemTray::acceptPendingActivated,
@@ -80,85 +133,71 @@ ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
 void
 ConversationsAdapter::safeInit()
 {
+    // TODO: remove these safeInits, they are possibly called
+    // multiple times during qml component inits
     conversationSmartListModel_ = new SmartListModel(this,
                                                      SmartListModel::Type::CONVERSATION,
                                                      lrcInstance_);
 
     Q_EMIT modelChanged(QVariant::fromValue(conversationSmartListModel_));
 
-    connect(&lrcInstance_->behaviorController(),
-            &BehaviorController::showChatView,
-            [this](const QString& accountId, const QString& convId) {
-                Q_EMIT showConversation(accountId, convId);
-            });
-
     connect(&lrcInstance_->behaviorController(),
             &BehaviorController::newUnreadInteraction,
             this,
-            &ConversationsAdapter::onNewUnreadInteraction);
+            &ConversationsAdapter::onNewUnreadInteraction,
+            Qt::UniqueConnection);
 
     connect(&lrcInstance_->behaviorController(),
             &BehaviorController::newReadInteraction,
             this,
-            &ConversationsAdapter::onNewReadInteraction);
+            &ConversationsAdapter::onNewReadInteraction,
+            Qt::UniqueConnection);
 
     connect(&lrcInstance_->behaviorController(),
             &BehaviorController::newTrustRequest,
             this,
-            &ConversationsAdapter::onNewTrustRequest);
+            &ConversationsAdapter::onNewTrustRequest,
+            Qt::UniqueConnection);
 
     connect(&lrcInstance_->behaviorController(),
             &BehaviorController::trustRequestTreated,
             this,
-            &ConversationsAdapter::onTrustRequestTreated);
+            &ConversationsAdapter::onTrustRequestTreated,
+            Qt::UniqueConnection);
 
     connect(lrcInstance_,
             &LRCInstance::currentAccountChanged,
             this,
-            &ConversationsAdapter::onCurrentAccountIdChanged);
+            &ConversationsAdapter::onCurrentAccountIdChanged,
+            Qt::UniqueConnection);
 
     connectConversationModel();
 
-    setProperty("currentTypeFilter",
-                QVariant::fromValue(lrcInstance_->getCurrentAccountInfo().profileInfo.type));
+    set_currentTypeFilter(lrcInstance_->getCurrentAccountInfo().profileInfo.type);
 }
 
 void
 ConversationsAdapter::backToWelcomePage()
 {
-    deselectConversation();
+    lrcInstance_->deselectConversation();
     Q_EMIT navigateToWelcomePageRequested();
 }
 
 void
-ConversationsAdapter::selectConversation(const QString& accountId, const QString& convUid)
+ConversationsAdapter::onCurrentAccountIdChanged()
 {
-    lrcInstance_->selectConversation(accountId, convUid);
-}
+    lrcInstance_->deselectConversation();
 
-void
-ConversationsAdapter::deselectConversation()
-{
-    if (lrcInstance_->get_selectedConvUid().isEmpty()) {
-        return;
-    }
+    convSrcModel_.reset(new ConversationListModel(lrcInstance_));
+    convModel_->bindSourceModel(convSrcModel_.get());
+    searchSrcModel_.reset(new SearchResultsListModel(lrcInstance_));
+    searchModel_->bindSourceModel(searchSrcModel_.get());
 
-    auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
-
-    if (currentConversationModel == nullptr) {
-        return;
-    }
-
-    lrcInstance_->set_selectedConvUid();
-}
-
-void
-ConversationsAdapter::onCurrentAccountIdChanged()
-{
     connectConversationModel();
 
-    setProperty("currentTypeFilter",
-                QVariant::fromValue(lrcInstance_->getCurrentAccountInfo().profileInfo.type));
+    updateConversationFilterData();
+
+    set_currentTypeFilter(lrcInstance_->getCurrentAccountInfo().profileInfo.type);
 }
 
 void
@@ -189,11 +228,9 @@ ConversationsAdapter::onNewUnreadInteraction(const QString& accountId,
         auto onClicked = [this, accountId, convUid, uri = interaction.authorUri] {
             Q_EMIT lrcInstance_->notificationClicked();
             const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId);
-            if (!convInfo.uid.isEmpty()) {
-                selectConversation(accountId, convInfo.uid);
-                Q_EMIT lrcInstance_->updateSmartList();
-                Q_EMIT modelSorted(convInfo.uid);
-            }
+            if (convInfo.uid.isEmpty())
+                return;
+            lrcInstance_->selectConversation(convInfo.uid, accountId);
         };
         systemTray_->showNotification(interaction.body, from, onClicked);
 #endif
@@ -209,6 +246,10 @@ ConversationsAdapter::onNewReadInteraction(const QString& accountId,
     // hide notification
     auto notifId = QString("%1;%2;%3").arg(accountId).arg(convUid).arg(interactionId);
     systemTray_->hideNotification(notifId);
+#else
+    Q_UNUSED(accountId)
+    Q_UNUSED(convUid)
+    Q_UNUSED(interactionId)
 #endif
 }
 
@@ -227,6 +268,9 @@ ConversationsAdapter::onNewTrustRequest(const QString& accountId, const QString&
                                       NotificationType::REQUEST,
                                       Utils::QImageToByteArray(contactPhoto));
     }
+#else
+    Q_UNUSED(accountId)
+    Q_UNUSED(peerUri)
 #endif
 }
 
@@ -237,6 +281,9 @@ ConversationsAdapter::onTrustRequestTreated(const QString& accountId, const QStr
     // hide notification
     auto notifId = QString("%1;%2").arg(accountId).arg(peerUri);
     systemTray_->hideNotification(notifId);
+#else
+    Q_UNUSED(accountId)
+    Q_UNUSED(peerUri)
 #endif
 }
 
@@ -244,46 +291,30 @@ void
 ConversationsAdapter::onModelChanged()
 {
     conversationSmartListModel_->fillConversationsList();
-    updateConversationsFilterWidget();
-
-    auto* convModel = lrcInstance_->getCurrentConversationModel();
-    const auto& convInfo = lrcInstance_->getConversationFromConvUid(
-        lrcInstance_->get_selectedConvUid());
-
-    if (convInfo.uid.isEmpty() || convInfo.participants.isEmpty()) {
-        return;
-    }
-    const auto contactURI = convInfo.participants[0];
-    if (contactURI.isEmpty()
-        || convModel->owner.contactModel->getContact(contactURI).profileInfo.type
-               == lrc::api::profile::Type::TEMPORARY) {
-        return;
-    }
-    Q_EMIT modelSorted(QVariant::fromValue(convInfo.uid));
+    updateConversationFilterData();
 }
 
 void
 ConversationsAdapter::onProfileUpdated(const QString& contactUri)
 {
+    // TODO: this will need a dataChanged call to keep the avatar
+    // updated. previously, 'reload-smartlist' was invoked here
     conversationSmartListModel_->updateContactAvatarUid(contactUri);
-    Q_EMIT updateListViewRequested();
 }
 
 void
 ConversationsAdapter::onConversationUpdated(const QString&)
 {
-    updateConversationsFilterWidget();
-    Q_EMIT updateListViewRequested();
+    updateConversationFilterData();
 }
 
 void
 ConversationsAdapter::onFilterChanged()
 {
     conversationSmartListModel_->fillConversationsList();
-    updateConversationsFilterWidget();
+    updateConversationFilterData();
     if (!lrcInstance_->get_selectedConvUid().isEmpty())
         Q_EMIT indexRepositionRequested();
-    Q_EMIT updateListViewRequested();
 }
 
 void
@@ -304,10 +335,9 @@ ConversationsAdapter::onConversationCleared(const QString& convUid)
 {
     // If currently selected, switch to welcome screen (deselecting
     // current smartlist item).
-    if (convUid != lrcInstance_->get_selectedConvUid()) {
-        return;
+    if (convUid == lrcInstance_->get_selectedConvUid()) {
+        lrcInstance_->deselectConversation();
     }
-    backToWelcomePage();
 }
 
 void
@@ -319,26 +349,94 @@ ConversationsAdapter::onSearchStatusChanged(const QString& status)
 void
 ConversationsAdapter::onSearchResultUpdated()
 {
+    // currently for contact pickers
     conversationSmartListModel_->fillConversationsList();
-    Q_EMIT updateListViewRequested();
+
+    // smartlist search results
+    searchSrcModel_->onSearchResultsUpdated();
 }
 
 void
-ConversationsAdapter::updateConversationsFilterWidget()
+ConversationsAdapter::updateConversationFilterData()
 {
-    // Update status of "Conversations" and "Invitations".
-    auto invites = lrcInstance_->getCurrentAccountInfo().contactModel->pendingRequestCount();
-    if (invites == 0 && currentTypeFilter_ == lrc::api::profile::Type::PENDING) {
-        setProperty("currentTypeFilter", QVariant::fromValue(lrc::api::profile::Type::RING));
+    // TODO: this may be further spliced to respond separately to
+    // incoming messages and invites
+    // total unread message and pending invite counts, and tab selection
+    auto& accountInfo = lrcInstance_->getCurrentAccountInfo();
+    int totalUnreadMessages {0};
+    if (accountInfo.profileInfo.type != profile::Type::SIP) {
+        auto& convModel = accountInfo.conversationModel;
+        auto conversations = convModel->getFilteredConversations(profile::Type::RING, false);
+        conversations.for_each([&totalUnreadMessages](const conversation::Info& conversation) {
+            totalUnreadMessages += conversation.unreadMessages;
+        });
     }
-    showConversationTabs(invites);
+    set_totalUnreadMessageCount(totalUnreadMessages);
+    set_pendingRequestCount(accountInfo.contactModel->pendingRequestCount());
+    if (pendingRequestCount_ == 0 && currentTypeFilter_ == profile::Type::PENDING) {
+        set_currentTypeFilter(profile::Type::RING);
+    }
+}
+
+void
+ConversationsAdapter::setFilter(const QString& filterString)
+{
+    convModel_->setFilter(filterString);
+    searchSrcModel_->setFilter(filterString);
 }
 
 void
-ConversationsAdapter::refill()
+ConversationsAdapter::setTypeFilter(const profile::Type& typeFilter)
 {
-    if (conversationSmartListModel_)
-        conversationSmartListModel_->fillConversationsList();
+    convModel_->setTypeFilter(typeFilter);
+}
+
+QVariantMap
+ConversationsAdapter::getConvInfoMap(const QString& convId)
+{
+    const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
+    if (convInfo.participants.empty())
+        return {};
+    auto peerUri = convInfo.participants[0];
+    ContactModel* contactModel {nullptr};
+    contact::Info contact {};
+    try {
+        const auto& accountInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
+        contactModel = accountInfo.contactModel.get();
+        contact = contactModel->getContact(peerUri);
+    } catch (...) {
+        return {};
+    }
+    bool isAudioOnly {false};
+    if (!convInfo.uid.isEmpty()) {
+        auto* call = lrcInstance_->getCallInfoForConversation(convInfo);
+        if (call) {
+            isAudioOnly = call->isAudioOnly;
+        }
+    }
+    bool callStackViewShouldShow {false};
+    call::Status callState {};
+    if (!convInfo.callId.isEmpty()) {
+        auto* callModel = lrcInstance_->getCurrentCallModel();
+        const auto& call = callModel->getCall(convInfo.callId);
+        callStackViewShouldShow = callModel->hasCall(convInfo.callId)
+                                  && ((!call.isOutgoing
+                                       && (call.status == call::Status::IN_PROGRESS
+                                           || call.status == call::Status::PAUSED
+                                           || call.status == call::Status::INCOMING_RINGING))
+                                      || (call.isOutgoing && call.status != call::Status::ENDED));
+        callState = call.status;
+    }
+    // WARNING: not swarm ready
+    // titles should come from conversation, not contact model
+    return {{"convId", convId},
+            {"bestId", contactModel->bestIdForContact(peerUri)},
+            {"bestName", contactModel->bestNameForContact(peerUri)},
+            {"uri", peerUri},
+            {"contactType", static_cast<int>(contact.profileInfo.type)},
+            {"isAudioOnly", isAudioOnly},
+            {"callState", static_cast<int>(callState)},
+            {"callStackViewShouldShow", callStackViewShouldShow}};
 }
 
 bool
@@ -402,7 +500,7 @@ ConversationsAdapter::connectConversationModel(bool updateFilter)
                      Qt::UniqueConnection);
 
     if (updateFilter) {
-        currentTypeFilter_ = lrc::api::profile::Type::INVALID;
+        currentTypeFilter_ = profile::Type::INVALID;
     }
     return true;
 }
@@ -421,8 +519,8 @@ ConversationsAdapter::updateConversationForNewContact(const QString& convUid)
             const auto contact = convModel->owner.contactModel->getContact(convInfo.participants[0]);
             if (!contact.profileInfo.uri.isEmpty()
                 && contact.profileInfo.uri == lrcInstance_->get_selectedConvUid()) {
-                lrcInstance_->set_selectedConvUid(convUid);
-                convModel->selectConversation(convUid);
+                lrcInstance_->selectConversation(convUid, convInfo.accountId);
+                convModel_->selectSourceRow(lrcInstance_->indexOf(convUid));
             }
         } catch (...) {
             return;
diff --git a/src/conversationsadapter.h b/src/conversationsadapter.h
index 32de1980eb04930c7f680c21b1308fec2c048ba7..11501ab6d91a4a666f2e95d54a1a8c60484c18db 100644
--- a/src/conversationsadapter.h
+++ b/src/conversationsadapter.h
@@ -1,6 +1,7 @@
-/*!
+/*
  * Copyright (C) 2020 by Savoir-faire Linux
  * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@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,6 +22,8 @@
 #include "lrcinstance.h"
 #include "qmladapterbase.h"
 #include "smartlistmodel.h"
+#include "conversationlistmodel.h"
+#include "searchresultslistmodel.h"
 
 #include <QObject>
 #include <QString>
@@ -30,9 +33,10 @@ class SystemTray;
 class ConversationsAdapter final : public QmlAdapterBase
 {
     Q_OBJECT
+    QML_PROPERTY(lrc::api::profile::Type, currentTypeFilter)
+    QML_PROPERTY(int, totalUnreadMessageCount)
+    QML_PROPERTY(int, pendingRequestCount)
 
-    Q_PROPERTY(lrc::api::profile::Type currentTypeFilter MEMBER currentTypeFilter_ NOTIFY
-                   currentTypeFilterChanged)
 public:
     explicit ConversationsAdapter(SystemTray* systemTray,
                                   LRCInstance* instance,
@@ -44,25 +48,22 @@ protected:
 
 public:
     Q_INVOKABLE bool connectConversationModel(bool updateFilter = true);
-    Q_INVOKABLE void selectConversation(const QString& accountId, const QString& uid);
-    Q_INVOKABLE void deselectConversation();
-    Q_INVOKABLE void refill();
-    Q_INVOKABLE void updateConversationsFilterWidget();
+    Q_INVOKABLE void setFilter(const QString& filterString);
+    Q_INVOKABLE void setTypeFilter(const profile::Type& typeFilter);
+    Q_INVOKABLE QVariantMap getConvInfoMap(const QString& convId);
 
 Q_SIGNALS:
     void showConversation(const QString& accountId, const QString& convUid);
-    void showConversationTabs(bool visible);
     void showSearchStatus(const QString& status);
 
     void modelChanged(const QVariant& model);
-    void modelSorted(const QVariant& uid);
-    void updateListViewRequested();
     void navigateToWelcomePageRequested();
-    void currentTypeFilterChanged();
     void indexRepositionRequested();
 
 private Q_SLOTS:
     void onCurrentAccountIdChanged();
+
+    // cross-account slots
     void onNewUnreadInteraction(const QString& accountId,
                                 const QString& convUid,
                                 uint64_t interactionId,
@@ -73,6 +74,7 @@ private Q_SLOTS:
     void onNewTrustRequest(const QString& accountId, const QString& peerUri);
     void onTrustRequestTreated(const QString& accountId, const QString& peerUri);
 
+    // per-account slots
     void onModelChanged();
     void onProfileUpdated(const QString&);
     void onConversationUpdated(const QString&);
@@ -83,13 +85,18 @@ private Q_SLOTS:
     void onSearchStatusChanged(const QString&);
     void onSearchResultUpdated();
 
+    void updateConversationFilterData();
+
 private:
     void backToWelcomePage();
     void updateConversationForNewContact(const QString& convUid);
 
     SmartListModel* conversationSmartListModel_;
 
-    lrc::api::profile::Type currentTypeFilter_ {};
-
     SystemTray* systemTray_;
+
+    QScopedPointer<ConversationListModel> convSrcModel_;
+    QScopedPointer<ConversationListProxyModel> convModel_;
+    QScopedPointer<SearchResultsListModel> searchSrcModel_;
+    QScopedPointer<SelectableListProxyModel> searchModel_;
 };
diff --git a/src/lrcinstance.cpp b/src/lrcinstance.cpp
index 738701f32539322bdcac2c435a5a66c3c8a68bd8..facb7be48f2b4a0fe20d5e1214ab096fc5964553 100644
--- a/src/lrcinstance.cpp
+++ b/src/lrcinstance.cpp
@@ -219,6 +219,12 @@ LRCInstance::getCurrentCallModel()
     return getCurrentAccountInfo().callModel.get();
 }
 
+ContactModel*
+LRCInstance::getCurrentContactModel()
+{
+    return getCurrentAccountInfo().contactModel.get();
+}
+
 const QString&
 LRCInstance::getCurrAccId()
 {
@@ -301,6 +307,18 @@ LRCInstance::getCurrAccConfig()
     return getCurrentAccountInfo().confProperties;
 }
 
+int
+LRCInstance::indexOf(const QString& convId)
+{
+    auto& convs = getCurrentConversationModel()->getConversations();
+    auto it = std::find_if(convs.begin(),
+                           convs.end(),
+                           [convId](const lrc::api::conversation::Info& conv) {
+                               return conv.uid == convId;
+                           });
+    return it != convs.end() ? std::distance(convs.begin(), it) : -1;
+}
+
 void
 LRCInstance::subscribeToDebugReceived()
 {
@@ -353,6 +371,8 @@ LRCInstance::setContentDraft(const QString& convUid,
 {
     auto draftKey = accountId + "_" + convUid;
     contentDrafts_[draftKey] = content;
+    // this signal is only needed to update the current smartlist
+    Q_EMIT draftSaved(convUid);
 }
 
 void
@@ -374,41 +394,24 @@ LRCInstance::poplastConference(const QString& confId)
 }
 
 void
-LRCInstance::selectConversation(const QString& accountId, const QString& convUid)
-{
-    const auto& convInfo = getConversationFromConvUid(convUid, accountId);
-
-    if (get_selectedConvUid() != 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);
-            set_selectedConvUid(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();
-            });
-            set_selectedConvUid();
-            setSelectedAccountId(convInfo.accountId);
-        } else {
-            selectConversation();
-        }
+LRCInstance::selectConversation(const QString& convId, const QString& accountId)
+{
+    // if the account is not currently selected, do that first, then
+    // proceed to select the conversation
+    if (!accountId.isEmpty() && accountId != getCurrAccId()) {
+        Utils::oneShotConnect(this, &LRCInstance::currentAccountChanged, [this, convId] {
+            set_selectedConvUid(convId);
+        });
+        setSelectedAccountId(accountId);
+        return;
     }
-    Q_EMIT conversationSelected();
+    set_selectedConvUid(convId);
+}
+
+void
+LRCInstance::deselectConversation()
+{
+    set_selectedConvUid();
 }
 
 void
diff --git a/src/lrcinstance.h b/src/lrcinstance.h
index 71119750d2bd22fa1700034f58fca154c453f3b3..e4cbe6e770c055fdae26a8c7fbbad9f5e8982575 100644
--- a/src/lrcinstance.h
+++ b/src/lrcinstance.h
@@ -72,6 +72,7 @@ public:
     NewAccountModel& accountModel();
     ConversationModel* getCurrentConversationModel();
     NewCallModel* getCurrentCallModel();
+    ContactModel* getCurrentContactModel();
     AVModel& avModel();
     PluginModel& pluginModel();
     BehaviorController& behaviorController();
@@ -95,15 +96,18 @@ public:
     const conversation::Info& getConversationFromCallId(const QString& callId,
                                                         const QString& accountId = {});
 
+    Q_INVOKABLE void selectConversation(const QString& convId, const QString& accountId = {});
+    Q_INVOKABLE void deselectConversation();
+
     const QString& getCurrAccId();
     void setSelectedAccountId(const QString& accountId = {});
-    void selectConversation(const QString& accountId, const QString& convUid);
     int getCurrentAccountIndex();
     void setAvatarForAccount(const QPixmap& avatarPixmap, const QString& accountID);
     void setCurrAccAvatar(const QPixmap& avatarPixmap);
     void setCurrAccAvatar(const QString& avatar);
     void setCurrAccDisplayName(const QString& displayName);
     const account::ConfProperties_t& getCurrAccConfig();
+    int indexOf(const QString& convId);
 
     void startAudioMeter(bool async);
     void stopAudioMeter(bool async);
@@ -121,15 +125,16 @@ Q_SIGNALS:
     void currentAccountChanged();
     void restoreAppRequested();
     void notificationClicked();
-    void updateSmartList();
     void quitEngineRequested();
-    void conversationSelected();
+    void conversationUpdated(const QString& convId, const QString& accountId);
+    void draftSaved(const QString& convId);
+    void contactBanned(const QString& uri);
 
 private:
     std::unique_ptr<Lrc> lrc_;
     std::unique_ptr<RenderManager> renderer_;
     std::unique_ptr<UpdateManager> updateManager_;
-    QString selectedAccountId_ {""};
+    QString selectedAccountId_ {};
     MapStringString contentDrafts_;
     MapStringString lastConferences_;
 
diff --git a/src/mainapplication.h b/src/mainapplication.h
index 881155d5c05fc05863e00b4a883cecbf03e8ec45..58c8a67f61a0f205eaf4213c2d7e5dc0fe939587 100644
--- a/src/mainapplication.h
+++ b/src/mainapplication.h
@@ -34,6 +34,7 @@
 class ConnectivityMonitor;
 class AppSettingsManager;
 class SystemTray;
+class CallAdapter;
 
 // Provides information about the screen the app is displayed on
 class ScreenInfo : public QObject
@@ -98,4 +99,6 @@ private:
     SystemTray* systemTray_;
 
     ScreenInfo screenInfo_;
+
+    CallAdapter* callAdapter_;
 };
diff --git a/src/mainview/MainView.qml b/src/mainview/MainView.qml
index b1669e49c811ecb661d7536b477dab9402a6ef9a..1c565dbb5bafd7e3411a055ea7c1c96d6e356715 100644
--- a/src/mainview/MainView.qml
+++ b/src/mainview/MainView.qml
@@ -59,8 +59,6 @@ Rectangle {
 
     property string currentAccountId: AccountAdapter.currentAccountId
     onCurrentAccountIdChanged: {
-        var index = UtilsAdapter.getCurrAccList().indexOf(currentAccountId)
-        mainViewSidePanel.refreshAccountComboBox(index)
         if (inSettingsView) {
             settingsView.accountListChanged()
             settingsView.setSelected(settingsView.selectedMenu, true)
@@ -80,7 +78,7 @@ Rectangle {
     function showWelcomeView() {
         currentConvUID = ""
         callStackView.needToCloseInCallConversationAndPotentialWindow()
-        mainViewSidePanel.deselectConversationSmartList()
+        LRCInstance.deselectConversation()
         if (isPageInStack("callStackViewObject", sidePanelViewStack) ||
                 isPageInStack("communicationPageMessageWebView", sidePanelViewStack) ||
                 isPageInStack("communicationPageMessageWebView", mainViewStack) ||
@@ -140,8 +138,7 @@ Rectangle {
         if (checkCurrentCall && currentAccountIsCalling()) {
             var callConv = UtilsAdapter.getCallConvForAccount(
                         AccountAdapter.currentAccountId)
-            ConversationsAdapter.selectConversation(
-                        AccountAdapter.currentAccountId, callConv)
+            LRCInstance.selectConversation(callConv)
             CallAdapter.updateCall(callConv, currentAccountId)
         } else {
             showWelcomeView()
@@ -171,56 +168,50 @@ Rectangle {
         }
     }
 
-    // ConversationSmartListViewItemDelegate provides UI information
-    function setMainView(currentUserDisplayName, currentUserAlias, currentUID,
-                               callStackViewShouldShow, isAudioOnly, callState) {
+    function setMainView(convId) {
         if (!(communicationPageMessageWebView.jsLoaded)) {
             communicationPageMessageWebView.jsLoadedChanged.connect(
-                        function(currentUserDisplayName, currentUserAlias, currentUID,
-                                 callStackViewShouldShow, isAudioOnly, callState) {
-                            return function() {
-                                setMainView(currentUserDisplayName, currentUserAlias, currentUID,
-                                            callStackViewShouldShow, isAudioOnly, callState)
-                            }
-                        }(currentUserDisplayName, currentUserAlias, currentUID,
-                          callStackViewShouldShow, isAudioOnly, callState))
+                        function(convId) {
+                            return function() { setMainView(convId) }
+                        }(convId))
             return
         }
-
-        if (callStackViewShouldShow) {
+        var item = ConversationsAdapter.getConvInfoMap(convId)
+        if (item.convId === undefined)
+            return
+        communicationPageMessageWebView.headerUserAliasLabelText = item.bestName
+        communicationPageMessageWebView.headerUserUserNameLabelText = item.bestId
+        if (item.callStackViewShouldShow) {
             if (inSettingsView) {
                 toggleSettingsView()
             }
-            MessagesAdapter.setupChatView(currentUID)
-            communicationPageMessageWebView.headerUserAliasLabelText = currentUserAlias
-            communicationPageMessageWebView.headerUserUserNameLabelText = currentUserDisplayName
+            MessagesAdapter.setupChatView(convId)
             callStackView.setLinkedWebview(communicationPageMessageWebView)
             callStackView.responsibleAccountId = AccountAdapter.currentAccountId
-            callStackView.responsibleConvUid = currentUID
-            currentConvUID = currentUID
+            callStackView.responsibleConvUid = convId
+            currentConvUID = convId
 
-            if (callState === Call.Status.IN_PROGRESS || callState === Call.Status.PAUSED) {
-                CallAdapter.updateCall(currentUID, AccountAdapter.currentAccountId)
-                if (isAudioOnly)
+            if (item.callState === Call.Status.IN_PROGRESS ||
+                    item.callState === Call.Status.PAUSED) {
+                CallAdapter.updateCall(convId, AccountAdapter.currentAccountId)
+                if (item.isAudioOnly)
                     callStackView.showAudioCallPage()
                 else
                     callStackView.showVideoCallPage()
-            } else if (callState === Call.Status.INCOMING_RINGING) {
+            } else if (item.callState === Call.Status.INCOMING_RINGING) {
                 callStackView.showIncomingCallPage()
             } else {
-                callStackView.showOutgoingCallPage(callState)
+                callStackView.showOutgoingCallPage(item.callState)
             }
             pushCallStackView()
 
         } else if (!inSettingsView) {
-            if (currentConvUID !== currentUID) {
+            if (currentConvUID !== convId) {
                 callStackView.needToCloseInCallConversationAndPotentialWindow()
-                MessagesAdapter.setupChatView(currentUID)
-                communicationPageMessageWebView.headerUserAliasLabelText = currentUserAlias
-                communicationPageMessageWebView.headerUserUserNameLabelText = currentUserDisplayName
+                MessagesAdapter.setupChatView(convId)
                 pushCommunicationMessageWebView()
                 communicationPageMessageWebView.focusMessageWebView()
-                currentConvUID = currentUID
+                currentConvUID = convId
             } else if (isPageInStack("callStackViewObject", sidePanelViewStack)
                        || isPageInStack("callStackViewObject", mainViewStack)) {
                 callStackView.needToCloseInCallConversationAndPotentialWindow()
@@ -233,11 +224,16 @@ Rectangle {
     color: JamiTheme.backgroundColor
 
     Connections {
-        target: CallAdapter
+        target: LRCInstance
+
+        function onSelectedConvUidChanged() {
+            mainView.setMainView(LRCInstance.selectedConvUid)
+        }
 
-        // selectConversation causes UI update
-        function onCallSetupMainViewRequired(accountId, convUid) {
-            ConversationsAdapter.selectConversation(accountId, convUid)
+        function onConversationUpdated(convUid, accountId) {
+            if (convUid === LRCInstance.selectedConvUid &&
+                    accountId === currentAccountId)
+                mainView.setMainView(convUid)
         }
     }
 
@@ -291,12 +287,6 @@ Rectangle {
                     Connections {
                         target: AccountAdapter
 
-                        function onUpdateConversationForAddedContact() {
-                            MessagesAdapter.updateConversationForAddedContact()
-                            mainViewSidePanel.clearContactSearchBar()
-                            mainViewSidePanel.forceReselectConversationSmartListCurrentIndex()
-                        }
-
                         function onAccountStatusChanged(accountId) {
                             accountComboBox.resetAccountListModel(accountId)
                         }
@@ -438,17 +428,12 @@ Rectangle {
         Connections {
             target: MessagesAdapter
 
-            function onNeedToUpdateSmartList() {
-                mainViewSidePanel.forceUpdateConversationSmartListView()
-            }
-
             function onNavigateToWelcomePageRequested() {
                 backToMainView()
             }
 
             function onInvitationAccepted() {
                 mainViewSidePanel.selectTab(SidePanelTabBar.Conversations)
-                showWelcomeView()
             }
         }
 
diff --git a/src/mainview/components/AccountComboBox.qml b/src/mainview/components/AccountComboBox.qml
index 80df7b93fe3c16227c83dec9c27fb10b026a24b5..cbcd7f1dfdff76a5abe0bdfa7ead522f12f325be 100644
--- a/src/mainview/components/AccountComboBox.qml
+++ b/src/mainview/components/AccountComboBox.qml
@@ -34,7 +34,20 @@ Label {
     signal settingBtnClicked
     property alias popup: comboBoxPopup
 
-    // Reset accountListModel.
+    // TODO: remove these refresh hacks use QAbstractItemModels correctly
+    Connections {
+        target: AccountAdapter
+
+        function onCurrentAccountIdChanged() {
+            root.update()
+            resetAccountListModel(AccountAdapter.currentAccountId)
+        }
+
+        function onAccountStatusChanged(accountId) {
+            resetAccountListModel(accountId)
+        }
+    }
+
     function resetAccountListModel(accountId) {
         accountListModel.updateAvatarUid(accountId)
         accountListModel.reset()
diff --git a/src/mainview/components/AccountComboBoxPopup.qml b/src/mainview/components/AccountComboBoxPopup.qml
index 4db441bcd710ffbc10db432a160a613fb48ffd83..3b0128d445ea8d8d0de7348431313ed66940fe57 100644
--- a/src/mainview/components/AccountComboBoxPopup.qml
+++ b/src/mainview/components/AccountComboBoxPopup.qml
@@ -112,11 +112,11 @@ Popup {
         layer {
             enabled: true
             effect: DropShadow {
-                color: JamiTheme.shadowColor
-                verticalOffset: 2
-                horizontalOffset: 2
+                horizontalOffset: 3.0
+                verticalOffset: 3.0
+                radius: 16.0
                 samples: 16
-                radius: 10
+                color: JamiTheme.shadowColor
             }
         }
     }
diff --git a/src/mainview/components/BadgeNotifier.qml b/src/mainview/components/BadgeNotifier.qml
new file mode 100644
index 0000000000000000000000000000000000000000..90aa14327105979949d64a82c1150598465c5f38
--- /dev/null
+++ b/src/mainview/components/BadgeNotifier.qml
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@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 <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.14
+
+import net.jami.Constants 1.0
+
+Rectangle {
+    id: root
+
+    property real size
+    property int count: 0
+    property int lastCount: count
+    property bool populated: false
+    property bool animate: true
+
+    width: size
+    height: size
+
+    radius: JamiTheme.primaryRadius
+    color: JamiTheme.filterBadgeColor
+
+    visible: count > 0
+
+    Text {
+        id: countLabel
+
+        anchors.centerIn: root
+        text: count > 9 ? "…" : count
+        color: JamiTheme.filterBadgeTextColor
+        font.pointSize: JamiTheme.filterBadgeFontSize
+        font.weight: Font.ExtraBold
+    }
+
+    onCountChanged: {
+        if (count > lastCount && animate)
+            notifyAnim.start()
+        lastCount = count
+        if (!populated)
+            populated = true
+    }
+    ParallelAnimation {
+        id: notifyAnim
+
+        ColorAnimation {
+            target: root; properties: "color"
+            from: JamiTheme.filterBadgeTextColor
+            to: JamiTheme.filterBadgeColor
+            duration: 150; easing.type: Easing.InOutQuad
+        }
+        ColorAnimation {
+            target: countLabel; properties: "color"
+            from: JamiTheme.filterBadgeColor
+            to: JamiTheme.filterBadgeTextColor
+            duration: 150; easing.type: Easing.InOutQuad
+        }
+        NumberAnimation {
+            target: root; property: "y"
+            from: -3; to: 0
+            duration: 150; easing.type: Easing.InOutQuad
+        }
+    }
+}
diff --git a/src/mainview/components/CallOverlay.qml b/src/mainview/components/CallOverlay.qml
index b0c611e9515b6826d0113d280dab808e3458dfd0..1e03f290de5b9821e299b38ae6aae137fed5764a 100644
--- a/src/mainview/components/CallOverlay.qml
+++ b/src/mainview/components/CallOverlay.qml
@@ -399,7 +399,7 @@ Rectangle {
         onAddToConferenceButtonClicked: {
             // Create contact picker - conference.
             ContactPickerCreation.createContactPickerObjects(
-                        ContactPicker.ContactPickerType.JAMICONFERENCE,
+                        ContactList.CONFERENCE,
                         callOverlayRect)
             ContactPickerCreation.openContactPicker()
         }
@@ -517,7 +517,7 @@ Rectangle {
         onTransferCallButtonClicked: {
             // Create contact picker - sip transfer.
             ContactPickerCreation.createContactPickerObjects(
-                        ContactPicker.ContactPickerType.SIPTRANSFER,
+                        ContactList.TRANSFER,
                         callOverlayRect)
             ContactPickerCreation.openContactPicker()
         }
diff --git a/src/mainview/components/ContactPicker.qml b/src/mainview/components/ContactPicker.qml
index c2165b59b3be2abb0a7288581fddd900818c0e20..24110c82f79dc763aa46f35fa25255d01043fca7 100644
--- a/src/mainview/components/ContactPicker.qml
+++ b/src/mainview/components/ContactPicker.qml
@@ -30,15 +30,7 @@ import "../../commoncomponents"
 Popup {
     id: contactPickerPopup
 
-    property int type: ContactPicker.ContactPickerType.JAMICONFERENCE
-
-
-    // Important to keep it one, since enum in c++ starts at one for conferences.
-    enum ContactPickerType {
-        CONVERSATION = 0,
-        JAMICONFERENCE,
-        SIPTRANSFER
-    }
+    property int type: ContactList.CONFERENCE
 
     contentWidth: 250
     contentHeight: contactPickerPopupRectColumnLayout.height + 50
@@ -89,9 +81,9 @@ Popup {
 
                 text: {
                     switch(type) {
-                    case ContactPicker.ContactPickerType.JAMICONFERENCE:
+                    case ContactList.CONFERENCE:
                         return qsTr("Add to conference")
-                    case ContactPicker.ContactPickerType.SIPTRANSFER:
+                    case ContactList.TRANSFER:
                         return qsTr("Transfer this call")
                     default:
                         return qsTr("Add default moderator")
diff --git a/src/mainview/components/ContactPickerItemDelegate.qml b/src/mainview/components/ContactPickerItemDelegate.qml
index bd3aa2379d2eb56dcf166d975d04e8c310890aed..d71bd2bcf246f5ccacb7de7337d0bded6f4e1da4 100644
--- a/src/mainview/components/ContactPickerItemDelegate.qml
+++ b/src/mainview/components/ContactPickerItemDelegate.qml
@@ -66,7 +66,7 @@ ItemDelegate {
                 font: contactPickerContactName.font
                 elide: Text.ElideMiddle
                 elideWidth: contactPickerContactInfoRect.width
-                text: DisplayName
+                text: BestName
             }
 
             color: JamiTheme.textColor
@@ -88,7 +88,7 @@ ItemDelegate {
                 font: contactPickerContactId.font
                 elide: Text.ElideMiddle
                 elideWidth: contactPickerContactInfoRect.width
-                text: DisplayID == DisplayName ? "" : DisplayID
+                text: BestId == BestName ? "" : BestId
             }
 
             text: textMetricsContactPickerContactId.elidedText
diff --git a/src/mainview/components/ContactSearchBar.qml b/src/mainview/components/ContactSearchBar.qml
index 6cdc25a6a9845421d6f2bf0875c35830c404bb52..1f9dbc7545021c0df6989d10864314eac85f709c 100644
--- a/src/mainview/components/ContactSearchBar.qml
+++ b/src/mainview/components/ContactSearchBar.qml
@@ -32,6 +32,8 @@ Rectangle {
     signal contactSearchBarTextChanged(string text)
     signal returnPressedWhileSearching
 
+    property alias textContent: contactSearchBar.text
+
     function clearText() {
         contactSearchBar.clear()
         fakeFocus.forceActiveFocus()
@@ -108,10 +110,11 @@ Rectangle {
         anchors.right: root.right
         anchors.rightMargin: 10
 
-        preferredSize: 20
+        preferredSize: 21
         radius: JamiTheme.primaryRadius
 
         visible: contactSearchBar.text.length
+        opacity: visible ? 1 : 0
 
         normalColor: root.color
         imageColor: JamiTheme.primaryForegroundColor
@@ -120,6 +123,10 @@ Rectangle {
         toolTipText: JamiStrings.clearText
 
         onClicked: contactSearchBar.clear()
+
+        Behavior on opacity {
+            NumberAnimation { duration: 500; easing.type: Easing.OutCubic }
+        }
     }
 
     Shortcut {
diff --git a/src/mainview/components/ConversationListView.qml b/src/mainview/components/ConversationListView.qml
new file mode 100644
index 0000000000000000000000000000000000000000..48981cd50695bd632164081312fcdeba055086e8
--- /dev/null
+++ b/src/mainview/components/ConversationListView.qml
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@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 <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+import QtQuick.Layouts 1.14
+
+import net.jami.Models 1.0
+import net.jami.Adapters 1.0
+import net.jami.Constants 1.0
+
+ListView {
+    id: root
+
+    // the following should be marked required (Qtver >= 5.15)
+    // along with `required model`
+    property string headerLabel
+    property bool headerVisible
+
+    delegate: SmartListItemDelegate {}
+    currentIndex: model.currentFilteredRow
+
+    clip: true
+    maximumFlickVelocity: 1024
+    ScrollIndicator.vertical: ScrollIndicator {}
+
+    // highlight selection
+    // down and hover states are done within the delegate
+    highlight: Rectangle {
+        width: ListView.view ? ListView.view.width : 0
+        color: JamiTheme.selectedColor
+    }
+    highlightMoveDuration: 60
+
+    headerPositioning: ListView.OverlayHeader
+    header: Rectangle {
+        z: 2
+        color: JamiTheme.backgroundColor
+        visible: root.headerVisible
+        width: root.width
+        height: root.headerVisible ? 20 : 0
+        Text {
+            anchors {
+                left: parent.left
+                leftMargin: 16
+                verticalCenter: parent.verticalCenter
+            }
+            text: headerLabel + " (" + root.count + ")"
+            font.pointSize: JamiTheme.smartlistItemFontSize
+            font.weight: Font.DemiBold
+            color: JamiTheme.textColor
+        }
+    }
+
+    Connections {
+        target: model
+
+        // actually select the conversation
+        function onValidSelectionChanged() {
+            var row = model.currentFilteredRow
+            var convId = model.dataForRow(row, ConversationList.UID)
+            LRCInstance.selectConversation(convId)
+        }
+    }
+
+    onCountChanged: positionViewAtBeginning()
+
+    Component.onCompleted: {
+        // TODO: remove this
+        ConversationsAdapter.setQmlObject(this)
+    }
+
+    add: Transition {
+        NumberAnimation {
+            property: "opacity"; from: 0; to: 1.0
+            duration: JamiTheme.smartListTransitionDuration
+        }
+    }
+
+    displaced: Transition {
+        NumberAnimation {
+            properties: "x,y"; easing.type: Easing.OutCubic
+            duration: JamiTheme.smartListTransitionDuration
+        }
+        NumberAnimation {
+            property: "opacity"; to: 1.0
+            duration: JamiTheme.smartListTransitionDuration * (1 - from)
+        }
+    }
+
+    Behavior on opacity {
+        NumberAnimation {
+            easing.type: Easing.OutCubic
+            duration: 2 * JamiTheme.smartListTransitionDuration
+        }
+    }
+
+    function openContextMenuAt(x, y, delegate) {
+        var mappedCoord = root.mapFromItem(delegate, x, y)
+        contextMenu.openMenuAt(mappedCoord.x, mappedCoord.y)
+    }
+
+    ConversationSmartListContextMenu {
+        id: contextMenu
+
+        function openMenuAt(x, y) {
+            contextMenu.x = x
+            contextMenu.y = y
+
+            // TODO:
+            // - accountId, convId only
+            // - userProfile dialog should use a loader/popup
+
+            var row = root.indexAt(x, y + root.contentY)
+            var item = {
+                "convId": model.dataForRow(row, ConversationList.UID),
+                "displayId": model.dataForRow(row, ConversationList.BestId),
+                "displayName": model.dataForRow(row, ConversationList.BestName),
+                "uri": model.dataForRow(row, ConversationList.URI),
+                "contactType": model.dataForRow(row, ConversationList.ContactType),
+            }
+
+            responsibleAccountId = AccountAdapter.currentAccountId
+            responsibleConvUid = item.convId
+            contactType = item.contactType
+
+            userProfile.responsibleConvUid = item.convId
+            userProfile.aliasText = item.displayName
+            userProfile.registeredNameText = item.displayId
+            userProfile.idText = item.uri
+            userProfile.contactImageUid = item.convId
+
+            openMenu()
+        }
+    }
+
+    Shortcut {
+        sequence: "Ctrl+Shift+X"
+        context: Qt.ApplicationShortcut
+        enabled: root.visible
+        onActivated: {
+            CallAdapter.placeCall()
+            communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
+        }
+    }
+
+    Shortcut {
+        sequence: "Ctrl+Shift+C"
+        context: Qt.ApplicationShortcut
+        enabled: root.visible
+        onActivated: {
+            CallAdapter.placeAudioOnlyCall()
+            communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
+        }
+    }
+
+    Shortcut {
+        sequence: "Ctrl+Shift+L"
+        context: Qt.ApplicationShortcut
+        enabled: root.visible
+        onActivated: MessagesAdapter.clearConversationHistory(
+                         AccountAdapter.currentAccountId,
+                         UtilsAdapter.getCurrConvId())
+    }
+
+    Shortcut {
+        sequence: "Ctrl+Shift+B"
+        context: Qt.ApplicationShortcut
+        enabled: root.visible
+        onActivated: {
+            MessagesAdapter.blockConversation(UtilsAdapter.getCurrConvId())
+        }
+    }
+
+    Shortcut {
+        sequence: "Ctrl+Shift+Delete"
+        context: Qt.ApplicationShortcut
+        enabled: root.visible
+        onActivated: MessagesAdapter.removeConversation(
+                         AccountAdapter.currentAccountId,
+                         UtilsAdapter.getCurrConvId(),
+                         false)
+    }
+
+    Shortcut {
+        sequence: "Ctrl+Down"
+        context: Qt.ApplicationShortcut
+        enabled: root.visible
+        onActivated: {
+            if (currentIndex + 1 >= count)
+                return
+            model.select(currentIndex + 1)
+        }
+    }
+
+    Shortcut {
+        sequence: "Ctrl+Up"
+        context: Qt.ApplicationShortcut
+        enabled: root.visible
+        onActivated: {
+            if (currentIndex <= 0)
+                return
+            model.select(currentIndex - 1)
+        }
+    }
+}
diff --git a/src/mainview/components/ConversationSmartListContextMenu.qml b/src/mainview/components/ConversationSmartListContextMenu.qml
index ab4200d09c0c0438804d4cd3d4328a6bdd5a19c5..c4a3bce54cbcb5f9c2ecc1bc13a02354b5dccad6 100644
--- a/src/mainview/components/ConversationSmartListContextMenu.qml
+++ b/src/mainview/components/ConversationSmartListContextMenu.qml
@@ -34,6 +34,8 @@ Item {
     property string responsibleConvUid: ""
     property int contactType: Profile.Type.INVALID
 
+    function isOpen() { return ContextMenuGenerator.getMenu().visible }
+
     function openMenu() {
         ContextMenuGenerator.initMenu()
         var hasCall = UtilsAdapter.getCallId(responsibleAccountId, responsibleConvUid) !== ""
@@ -41,18 +43,14 @@ Item {
             ContextMenuGenerator.addMenuItem(qsTr("Start video call"),
                                              "qrc:/images/icons/videocam-24px.svg",
                                              function (){
-                                                 ConversationsAdapter.selectConversation(
-                                                             responsibleAccountId,
-                                                             responsibleConvUid, false)
+                                                 LRCInstance.selectConversation(responsibleConvUid, responsibleAccountId)
                                                  CallAdapter.placeCall()
                                                  communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
                                              })
             ContextMenuGenerator.addMenuItem(qsTr("Start audio call"),
                                              "qrc:/images/icons/place_audiocall-24px.svg",
                                              function (){
-                                                 ConversationsAdapter.selectConversation(
-                                                             responsibleAccountId,
-                                                             responsibleConvUid, false)
+                                                 LRCInstance.selectConversation(responsibleConvUid, responsibleAccountId)
                                                  CallAdapter.placeAudioOnlyCall()
                                                  communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
                                              })
@@ -116,7 +114,7 @@ Item {
                                                  })
             }
             ContextMenuGenerator.addMenuSeparator()
-            ContextMenuGenerator.addMenuItem(qsTr("Profile"),
+            ContextMenuGenerator.addMenuItem(qsTr("Contact details"),
                                              "qrc:/images/icons/person-24px.svg",
                                              function (){
                                                  userProfile.open()
diff --git a/src/mainview/components/ConversationSmartListView.qml b/src/mainview/components/ConversationSmartListView.qml
deleted file mode 100644
index a4f4dda5a28f64ec466b0594647854b174b499cc..0000000000000000000000000000000000000000
--- a/src/mainview/components/ConversationSmartListView.qml
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2020 by Savoir-faire Linux
- * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
- * Author: Andreas Traczyk <andreas.traczyk@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 <https://www.gnu.org/licenses/>.
- */
-
-import QtQuick 2.14
-import QtQuick.Controls 2.14
-import QtQuick.Layouts 1.14
-
-import net.jami.Models 1.0
-import net.jami.Adapters 1.0
-
-ListView {
-    id: root
-
-    signal needToDeselectItems
-    signal forceUpdatePotentialInvalidItem
-
-    // Refresh all items within the model.
-    function updateListView() {
-        if (!root.model)
-            return
-        root.model.dataChanged(
-                    root.model.index(0, 0),
-                    root.model.index(
-                    root.model.rowCount() - 1, 0))
-        root.forceUpdatePotentialInvalidItem()
-    }
-
-    function repositionIndex(uid = "") {
-        // Only update index if it has changed
-        var currentI = root.currentIndex
-        if (uid === "")
-            uid = mainView.currentConvUID
-        root.currentIndex = -1
-        updateListView()
-        for (var i = 0; i < count; i++) {
-            if (root.model.data(
-                root.model.index(i, 0), SmartListModel.UID) === uid) {
-                root.currentIndex = i
-                break
-            }
-        }
-    }
-
-    ConversationSmartListContextMenu {
-        id: smartListContextMenu
-    }
-
-    Connections {
-        target: ConversationsAdapter
-
-        function onModelChanged(model) {
-            root.model = model
-        }
-
-        // When the model has been sorted, we need to adjust the focus (currentIndex)
-        // to the previously focused conversation item.
-        function onModelSorted(uid) {
-            repositionIndex(uid)
-        }
-
-        function onUpdateListViewRequested() {
-            updateListView()
-        }
-
-        function onIndexRepositionRequested() {
-            repositionIndex()
-        }
-    }
-
-    Connections {
-        target: LRCInstance
-        function onUpdateSmartList() { updateListView() }
-    }
-
-    clip: true
-    maximumFlickVelocity: 1024
-
-    delegate: ConversationSmartListViewItemDelegate {
-        id: smartListItemDelegate
-
-        onUpdateContactAvatarUidRequested: root.model.updateContactAvatarUid(uid)
-    }
-
-    ScrollIndicator.vertical: ScrollIndicator {}
-
-    Shortcut {
-        sequence: "Ctrl+Shift+X"
-        context: Qt.ApplicationShortcut
-        enabled: root.visible
-        onActivated: {
-            CallAdapter.placeCall()
-            communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
-        }
-    }
-
-    Shortcut {
-        sequence: "Ctrl+Shift+C"
-        context: Qt.ApplicationShortcut
-        enabled: root.visible
-        onActivated: {
-            CallAdapter.placeAudioOnlyCall()
-            communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
-        }
-    }
-
-    Shortcut {
-        sequence: "Ctrl+Shift+L"
-        context: Qt.ApplicationShortcut
-        enabled: root.visible
-        onActivated: MessagesAdapter.clearConversationHistory(
-                         AccountAdapter.currentAccountId,
-                         UtilsAdapter.getCurrConvId())
-    }
-
-    Shortcut {
-        sequence: "Ctrl+Shift+B"
-        context: Qt.ApplicationShortcut
-        enabled: root.visible
-        onActivated: {
-            MessagesAdapter.blockConversation(UtilsAdapter.getCurrConvId())
-        }
-    }
-
-    Shortcut {
-        sequence: "Ctrl+Shift+Delete"
-        context: Qt.ApplicationShortcut
-        enabled: root.visible
-        onActivated: MessagesAdapter.removeConversation(
-                         AccountAdapter.currentAccountId,
-                         UtilsAdapter.getCurrConvId(),
-                         false)
-    }
-
-    Shortcut {
-        sequence: "Ctrl+Down"
-        context: Qt.ApplicationShortcut
-        enabled: root.visible
-        onActivated: {
-            if (currentIndex + 1 >= count)
-                return
-            root.currentIndex += 1
-        }
-    }
-
-    Shortcut {
-        sequence: "Ctrl+Up"
-        context: Qt.ApplicationShortcut
-        enabled: root.visible
-        onActivated: {
-            if (currentIndex <= 0)
-                return
-            root.currentIndex -= 1
-        }
-    }
-}
diff --git a/src/mainview/components/ConversationSmartListViewItemDelegate.qml b/src/mainview/components/ConversationSmartListViewItemDelegate.qml
deleted file mode 100644
index cc4ca6d4ff955e418f68a4a2428ad7c4ca3ecb0f..0000000000000000000000000000000000000000
--- a/src/mainview/components/ConversationSmartListViewItemDelegate.qml
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * Copyright (C) 2020 by Savoir-faire Linux
- * Author: Mingrui Zhang <mingrui.zhang@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 <https://www.gnu.org/licenses/>.
- */
-
-import QtQuick 2.14
-import QtQuick.Controls 2.14
-import QtQuick.Layouts 1.14
-
-import net.jami.Models 1.0
-import net.jami.Adapters 1.0
-import net.jami.Constants 1.0
-
-import "../../commoncomponents"
-
-ItemDelegate {
-    id: smartListItemDelegate
-    height: 72
-
-    property int lastInteractionPreferredWidth: 80
-
-    signal updateContactAvatarUidRequested(string uid)
-
-    property bool openedMenu: false
-
-    function convUid() {
-        return UID
-    }
-
-    Connections {
-        target: conversationSmartListView
-
-        // Hack, make sure that smartListItemDelegate does not show extra item
-        // when searching new contacts.
-        function onForceUpdatePotentialInvalidItem() {
-            smartListItemDelegate.visible =
-                    conversationSmartListView.model.rowCount() <= index ? false : true
-        }
-
-
-        // When currentIndex is -1, deselect items, if not, change select item
-        function onCurrentIndexChanged() {
-            if (conversationSmartListView.currentIndex === -1
-                    || conversationSmartListView.currentIndex !== index) {
-                itemSmartListBackground.color = Qt.binding(function () {
-                    return InCall ? Qt.lighter(JamiTheme.selectionBlue,
-                                               1.8) : JamiTheme.backgroundColor
-                })
-            } else {
-                itemSmartListBackground.color = Qt.binding(function () {
-                    return InCall ? Qt.lighter(JamiTheme.selectionBlue,
-                                               1.8) : JamiTheme.selectedColor
-                })
-                ConversationsAdapter.selectConversation(
-                            AccountAdapter.currentAccountId, UID)
-            }
-        }
-    }
-
-    Connections {
-        target: ConversationsAdapter
-
-        function onShowConversation(accountId, convUid) {
-            if (convUid === UID) {
-                mainView.setMainView(DisplayID == DisplayName ? "" : DisplayID,
-                            DisplayName, UID, CallStackViewShouldShow, IsAudioOnly, CallState)
-            }
-        }
-    }
-
-    AvatarImage {
-        id: conversationSmartListUserImage
-
-        anchors.left: parent.left
-        anchors.verticalCenter: parent.verticalCenter
-        anchors.leftMargin: 16
-
-        width: 40
-        height: 40
-
-        mode: AvatarImage.Mode.FromContactUri
-
-        showPresenceIndicator: Presence === undefined ? false : Presence
-
-        unreadMessagesCount: UnreadMessagesCount
-
-        Component.onCompleted: {
-            var contactUid = URI
-            if (ContactType === Profile.Type.TEMPORARY)
-                updateContactAvatarUidRequested(contactUid)
-            updateImage(contactUid, PictureUid)
-        }
-    }
-
-    RowLayout {
-        id: rowUsernameAndLastInteractionDate
-        anchors.left: conversationSmartListUserImage.right
-        anchors.leftMargin: 16
-        anchors.top: parent.top
-        anchors.topMargin: conversationSmartListUserLastInteractionMessage.text !== "" ?
-                               16 : parent.height/2-conversationSmartListUserName.height/2
-        anchors.right: parent.right
-        anchors.rightMargin: 10
-
-        Text {
-            id: conversationSmartListUserName
-            Layout.alignment: conversationSmartListUserLastInteractionMessage.text !== "" ?
-                                  Qt.AlignLeft : Qt.AlignLeft | Qt.AlignVCenter
-
-            TextMetrics {
-                id: textMetricsConversationSmartListUserName
-                font: conversationSmartListUserName.font
-                elide: Text.ElideRight
-                elideWidth: LastInteractionDate ? (smartListItemDelegate.width - lastInteractionPreferredWidth
-                                                   - conversationSmartListUserImage.width-32)
-                                                : smartListItemDelegate.width - lastInteractionPreferredWidth
-                text: DisplayName === undefined ? "" : DisplayName
-            }
-            text: textMetricsConversationSmartListUserName.elidedText
-            font.pointSize: JamiTheme.smartlistItemFontSize
-            color: JamiTheme.textColor
-        }
-
-        Text {
-            id: conversationSmartListUserLastInteractionDate
-            Layout.alignment: Qt.AlignRight
-            TextMetrics {
-                id: textMetricsConversationSmartListUserLastInteractionDate
-                font: conversationSmartListUserLastInteractionDate.font
-                elide: Text.ElideRight
-                elideWidth: lastInteractionPreferredWidth
-                text: LastInteractionDate === undefined ? "" : LastInteractionDate
-            }
-
-            text: textMetricsConversationSmartListUserLastInteractionDate.elidedText
-            font.pointSize: JamiTheme.textFontSize
-            color: JamiTheme.faddedLastInteractionFontColor
-        }
-    }
-
-    Text {
-        id: conversationSmartListUserLastInteractionMessage
-
-        anchors.left: conversationSmartListUserImage.right
-        anchors.leftMargin: 16
-        anchors.bottom: rowUsernameAndLastInteractionDate.bottom
-        anchors.bottomMargin: -20
-
-        TextMetrics {
-            id: textMetricsConversationSmartListUserLastInteractionMessage
-            font: conversationSmartListUserLastInteractionMessage.font
-            elide: Text.ElideRight
-            elideWidth: LastInteractionDate ? (smartListItemDelegate.width - lastInteractionPreferredWidth
-                                               - conversationSmartListUserImage.width-32)
-                                            : smartListItemDelegate.width - lastInteractionPreferredWidth
-            text: InCall ? UtilsAdapter.getCallStatusStr(CallState) : (Draft ? Draft : LastInteraction)
-        }
-
-        font.family: Qt.platform.os === "windows" ? "Segoe UI Emoji" : Qt.application.font.family
-        font.hintingPreference: Font.PreferNoHinting
-        text: textMetricsConversationSmartListUserLastInteractionMessage.elidedText
-        maximumLineCount: 1
-        font.pointSize: JamiTheme.textFontSize
-        color: Draft ? JamiTheme.draftRed : JamiTheme.faddedLastInteractionFontColor
-    }
-
-    background: Rectangle {
-        id: itemSmartListBackground
-        color: InCall ? Qt.lighter(JamiTheme.selectionBlue, 1.8) : JamiTheme.backgroundColor
-        implicitWidth: conversationSmartListView.width
-        implicitHeight: parent.height
-        border.width: 0
-    }
-
-    MouseArea {
-        id: mouseAreaSmartListItemDelegate
-
-        anchors.fill: parent
-        hoverEnabled: true
-        acceptedButtons: Qt.LeftButton | Qt.RightButton
-
-        function openContextMenu(mouse) {
-            openedMenu = true
-            smartListContextMenu.parent = mouseAreaSmartListItemDelegate
-
-            // Make menu pos at mouse.
-            var relativeMousePos = mapToItem(itemSmartListBackground,
-                                                mouse.x, mouse.y)
-            smartListContextMenu.x = relativeMousePos.x
-            smartListContextMenu.y = relativeMousePos.y
-            smartListContextMenu.responsibleAccountId = AccountAdapter.currentAccountId
-            smartListContextMenu.responsibleConvUid = UID
-            smartListContextMenu.contactType = ContactType
-            userProfile.responsibleConvUid = UID
-            userProfile.aliasText = DisplayName
-            userProfile.registeredNameText = DisplayID
-            userProfile.idText = URI
-            userProfile.contactImageUid = UID
-            smartListContextMenu.openMenu()
-        }
-
-        onPressed: {
-            if (!InCall) {
-                itemSmartListBackground.color = JamiTheme.pressColor
-            }
-        }
-        onDoubleClicked: {
-            if (!InCall) {
-                ConversationsAdapter.selectConversation(AccountAdapter.currentAccountId,
-                                                        UID,
-                                                        false)
-                if (AccountAdapter.currentAccountType === Profile.Type.SIP)
-                    CallAdapter.placeAudioOnlyCall()
-                else
-                    CallAdapter.placeCall()
-                communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
-            }
-        }
-        onPressAndHold: {
-            openContextMenu(mouse)
-        }
-        onReleased: {
-            if (!InCall) {
-                itemSmartListBackground.color = JamiTheme.selectionBlue
-            }
-            if (mouse.button === Qt.RightButton) {
-                openContextMenu(mouse)
-            } else if (mouse.button === Qt.LeftButton && !openedMenu) {
-                conversationSmartListView.currentIndex = -1
-                conversationSmartListView.currentIndex = index
-            }
-            openedMenu = false
-        }
-        onEntered: {
-            if (!InCall) {
-                itemSmartListBackground.color = JamiTheme.hoverColor
-            }
-        }
-        onExited: {
-            if (!InCall) {
-                if (conversationSmartListView.currentIndex !== index
-                        || conversationSmartListView.currentIndex === -1) {
-                    itemSmartListBackground.color = Qt.binding(function () {
-                        return InCall ? Qt.lighter(JamiTheme.selectionBlue,
-                                                   1.8) : JamiTheme.backgroundColor
-                    })
-                } else {
-                    itemSmartListBackground.color = Qt.binding(function () {
-                        return InCall ? Qt.lighter(JamiTheme.selectionBlue,
-                                                   1.8) : JamiTheme.selectedColor
-                    })
-                }
-            }
-        }
-    }
-}
diff --git a/src/mainview/components/FilterTabButton.qml b/src/mainview/components/FilterTabButton.qml
index 5f70ac661c3ef884e4f90f9e814bd7a513c0d2ab..19021fd32c9af1f4d2f8f19af68eb4b50e061b6e 100644
--- a/src/mainview/components/FilterTabButton.qml
+++ b/src/mainview/components/FilterTabButton.qml
@@ -20,10 +20,7 @@
 import QtQuick 2.14
 import QtQuick.Controls 2.14
 import QtQuick.Layouts 1.14
-import QtGraphicalEffects 1.14
 
-import net.jami.Models 1.0
-import net.jami.Adapters 1.0
 import net.jami.Constants 1.0
 
 import "../../commoncomponents"
@@ -34,7 +31,7 @@ TabButton {
     property var tabBar: undefined
     property alias labelText: label.text
     property alias acceleratorSequence: accelerator.sequence
-    property int badgeCount
+    property alias badgeCount: badge.count
     signal selected
 
     hoverEnabled: true
@@ -57,32 +54,17 @@ TabButton {
                 id: label
 
                 Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+                Layout.bottomMargin: 1
 
                 font.pointSize: JamiTheme.filterItemFontSize
-                color: Qt.lighter(JamiTheme.textColor,
-                                  root.down == true ? 1.0 : 1.5)
+                color: JamiTheme.textColor
+                opacity: root.down ? 1.0 : 0.5
             }
 
-            Rectangle {
-                id: badgeRect
-
-                readonly property real size: 20
-
+            BadgeNotifier {
+                id: badge
+                size: 20
                 Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
-
-                width: size
-                height: size
-                radius: JamiTheme.primaryRadius
-                color: JamiTheme.filterBadgeColor
-
-                visible: badgeCount > 0
-
-                Text {
-                    anchors.centerIn: badgeRect
-                    text: badgeCount > 9 ? "…" : badgeCount
-                    color: JamiTheme.filterBadgeTextColor
-                    font.pointSize: JamiTheme.filterBadgeFontSize
-                }
             }
         }
     }
@@ -91,9 +73,7 @@ TabButton {
         width: rect.width
         anchors.bottom: rect.bottom
         height: 2
-        color: root.down === true ?
-                   JamiTheme.textColor :
-                   "transparent"
+        color: root.down ? JamiTheme.textColor : "transparent"
     }
 
     Shortcut {
diff --git a/src/mainview/components/MessageWebView.qml b/src/mainview/components/MessageWebView.qml
index f2bad1d50d075e9161395f8c8e3864c52e85d76a..442d6abd6c994ed6a02884f50c186e3a64cefa8a 100644
--- a/src/mainview/components/MessageWebView.qml
+++ b/src/mainview/components/MessageWebView.qml
@@ -108,6 +108,14 @@ Rectangle {
         }
     }
 
+    Connections {
+        target: AccountAdapter
+
+        function onSelectedContactAdded(convId) {
+            MessagesAdapter.updateConversationForAddedContact()
+        }
+    }
+
     JamiFileDialog {
         id: jamiFileDialog
 
diff --git a/src/mainview/components/SidePanel.qml b/src/mainview/components/SidePanel.qml
index fde7cc28811487d456efeda43197ebc59e6326d7..d86ba5cb35252c8cdbd6bb027a3d7ed91ae2b513 100644
--- a/src/mainview/components/SidePanel.qml
+++ b/src/mainview/components/SidePanel.qml
@@ -1,6 +1,7 @@
 /*
- * Copyright (C) 2020 by Savoir-faire Linux
+ * Copyright (C) 2020-2021 by Savoir-faire Linux
  * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@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
@@ -32,60 +33,29 @@ Rectangle {
 
     color: JamiTheme.backgroundColor
 
-    property bool tabBarVisible: true
-    property int pendingRequestCount: 0
-    property int totalUnreadMessagesCount: 0
-
-    // Hack -> force redraw.
-    function forceReselectConversationSmartListCurrentIndex() {
-        var index = conversationSmartListView.currentIndex
-        conversationSmartListView.currentIndex = -1
-        conversationSmartListView.currentIndex = index
-    }
-
+    anchors.fill: parent
 
-    // For contact request conv to be focused correctly.
-    function setCurrentUidSmartListModelIndex() {
-        conversationSmartListView.currentIndex
-                = conversationSmartListView.model.currentUidSmartListModelIndex()
-    }
+    Connections {
+        target: AccountAdapter
 
-    function updatePendingRequestCount() {
-        pendingRequestCount = UtilsAdapter.getTotalPendingRequest()
-    }
+        function onCurrentAccountIdChanged() {
+            clearContactSearchBar()
+        }
 
-    function updateTotalUnreadMessagesCount() {
-        totalUnreadMessagesCount = UtilsAdapter.getTotalUnreadMessages()
+        function onSelectedContactAdded(convId) {
+            clearContactSearchBar()
+            LRCInstance.selectConversation(convId)
+        }
     }
 
     function clearContactSearchBar() {
         contactSearchBar.clearText()
     }
 
-    function refreshAccountComboBox(index) {
-        accountComboBox.update()
-        clearContactSearchBar()
-        accountComboBox.resetAccountListModel()
-    }
-
-    function deselectConversationSmartList() {
-        ConversationsAdapter.deselectConversation()
-        conversationSmartListView.currentIndex = -1
-    }
-
-    function forceUpdateConversationSmartListView() {
-        conversationSmartListView.updateListView()
-    }
-
     function selectTab(tabIndex) {
         sidePanelTabBar.selectTab(tabIndex)
     }
 
-    // Intended -> since strange behavior will happen without this for stackview.
-    anchors.top: parent.top
-    anchors.fill: parent
-
-    // Search bar container to embed search label
     ContactSearchBar {
         id: contactSearchBar
 
@@ -98,116 +68,114 @@ Rectangle {
         anchors.rightMargin: 15
 
         onContactSearchBarTextChanged: {
-            UtilsAdapter.setConversationFilter(text)
+            // not calling positionViewAtBeginning will cause
+            // sort animation visual bugs
+            conversationListView.positionViewAtBeginning()
+            ConversationsAdapter.setFilter(text)
         }
 
         onReturnPressedWhileSearching: {
-            var convUid = conversationSmartListView.itemAtIndex(0).convUid()
-            var currentAccountId = AccountAdapter.currentAccountId
-            ConversationsAdapter.selectConversation(currentAccountId, convUid)
-            conversationSmartListView.repositionIndex(convUid)
+            var listView = searchResultsListView.count ?
+                        searchResultsListView :
+                        conversationListView
+            if (listView.count)
+                listView.model.select(0)
         }
     }
 
     SidePanelTabBar {
         id: sidePanelTabBar
+
+        visible: ConversationsAdapter.pendingRequestCount &&
+                 !contactSearchBar.textContent
         anchors.top: contactSearchBar.bottom
-        anchors.topMargin: 10
+        anchors.topMargin: visible ? 10 : 0
         width: sidePanelRect.width
-        height: tabBarVisible ? 42 : 0
+        height: visible ? 42 : 0
     }
 
     Rectangle {
         id: searchStatusRect
 
-        visible: lblSearchStatus.text !== ""
+        visible: searchStatusText.text !== ""
 
-        anchors.top: tabBarVisible ? sidePanelTabBar.bottom : contactSearchBar.bottom
-        anchors.topMargin: tabBarVisible ? 0 : 10
+        anchors.top: sidePanelTabBar.bottom
+        anchors.topMargin: visible ? 10 : 0
         width: parent.width
-        height: 72
-
-        color: "transparent"
-
-        Image {
-            id: searchIcon
-            anchors.left: searchStatusRect.left
-            anchors.leftMargin: 24
-            anchors.verticalCenter: searchStatusRect.verticalCenter
-            width: 24
-            height: 24
-
-            layer {
-                enabled: true
-                effect: ColorOverlay {
-                    color: JamiTheme.textColor
-                }
-            }
+        height: visible ? 42 : 0
 
-            fillMode: Image.PreserveAspectFit
-            mipmap: true
-            source: "qrc:/images/icons/ic_baseline-search-24px.svg"
-        }
+        color: JamiTheme.backgroundColor
 
-        Label {
-            id: lblSearchStatus
+        Text {
+            id: searchStatusText
 
-            anchors.verticalCenter: searchStatusRect.verticalCenter
-            anchors.left: searchIcon.right
-            anchors.leftMargin: 24
-            width: searchStatusRect.width - searchIcon.width - 24*2 - 8
-            text: ""
+            anchors.verticalCenter: parent.verticalCenter
+            anchors.left: parent.left
+            anchors.leftMargin: 32
+            anchors.right: parent.right
+            anchors.rightMargin: 32
             color: JamiTheme.textColor
             wrapMode: Text.WordWrap
-            font.pointSize: JamiTheme.menuFontSize
+            font.pointSize: JamiTheme.filterItemFontSize
         }
+    }
 
-        MouseArea {
-            id: mouseAreaSearchRect
-
-            anchors.fill: parent
-            hoverEnabled: true
-
-            onReleased: {
-                searchStatusRect.color = Qt.binding(function(){return JamiTheme.normalButtonColor})
-            }
-
-            onEntered: {
-                searchStatusRect.color = Qt.binding(function(){return JamiTheme.hoverColor})
-            }
+    Connections {
+        target: ConversationsAdapter
 
-            onExited: {
-                searchStatusRect.color = Qt.binding(function(){return JamiTheme.backgroundColor})
-            }
+        function onShowSearchStatus(status) {
+            searchStatusText.text = status
         }
     }
 
-    ConversationSmartListView {
-        id: conversationSmartListView
+    ColumnLayout {
+        id: smartListLayout
 
-        anchors.top: searchStatusRect.visible ? searchStatusRect.bottom : (tabBarVisible ? sidePanelTabBar.bottom : contactSearchBar.bottom)
-        anchors.topMargin: (tabBarVisible || searchStatusRect.visible) ? 0 : 10
         width: parent.width
-        height: tabBarVisible ? sidePanelRect.height - sidePanelTabBar.height - contactSearchBar.height - 20 :
-                                sidePanelRect.height - contactSearchBar.height - 20
-
-        Connections {
-            target: ConversationsAdapter
-
-            function onShowConversationTabs(visible) {
-                tabBarVisible = visible
-                updatePendingRequestCount()
-                updateTotalUnreadMessagesCount()
+        anchors.top: searchStatusRect.bottom
+        anchors.topMargin: (sidePanelTabBar.visible ||
+                            searchStatusRect.visible) ? 0 : 12
+        anchors.bottom: parent.bottom
+
+        spacing: 4
+
+        ConversationListView {
+            id: searchResultsListView
+
+            visible: count
+            opacity: visible ? 1 :0
+
+            Layout.topMargin: 10
+            Layout.alignment: Qt.AlignTop
+            Layout.fillWidth: true
+            Layout.preferredHeight: visible ? contentHeight : 0
+            Layout.maximumHeight: {
+                var otherContentHeight = conversationListView.contentHeight + 16
+                if (conversationListView.visible)
+                    if (otherContentHeight < parent.height / 2)
+                        return parent.height - otherContentHeight
+                    else
+                        return parent.height / 2
+                else
+                    return parent.height
             }
 
-            function onShowSearchStatus(status) {
-                lblSearchStatus.text = status
-            }
+            model: SearchResultsListModel
+            headerLabel: JamiStrings.searchResults
+            headerVisible: visible
         }
 
-        Component.onCompleted: {
-            ConversationsAdapter.setQmlObject(this)
-            conversationSmartListView.currentIndex = -1
+        ConversationListView {
+            id: conversationListView
+
+            visible: count
+
+            Layout.preferredWidth: parent.width
+            Layout.fillHeight: true
+
+            model: ConversationListModel
+            headerLabel: JamiStrings.conversations
+            headerVisible: searchResultsListView.visible
         }
     }
 }
diff --git a/src/mainview/components/SidePanelTabBar.qml b/src/mainview/components/SidePanelTabBar.qml
index cd5f51ab9b1d52d6a3e614a45b1873f14288df65..04cd82dde18bba8581abe0bea4a0762e29d93226 100644
--- a/src/mainview/components/SidePanelTabBar.qml
+++ b/src/mainview/components/SidePanelTabBar.qml
@@ -28,31 +28,18 @@ import net.jami.Constants 1.0
 
 import "../../commoncomponents"
 
-// TODO:
-// - totalUnreadMessagesCount and pendingRequestCount could be
-//   properties of ConversationsAdapter
-// - onCurrentTypeFilterChanged shouldn't need to update the smartlist
-// - tabBarVisible could be factored out
-
 TabBar {
     id: tabBar
 
+    property int currentTypeFilter: ConversationsAdapter.currentTypeFilter
+
+    currentIndex: 0
+
     enum TabIndex {
         Conversations,
         Requests
     }
 
-    Connections {
-        target: ConversationsAdapter
-
-        function onCurrentTypeFilterChanged() {
-            pageOne.down = ConversationsAdapter.currentTypeFilter !==  Profile.Type.PENDING
-            pageTwo.down = ConversationsAdapter.currentTypeFilter ===  Profile.Type.PENDING
-            setCurrentUidSmartListModelIndex()
-            forceReselectConversationSmartListCurrentIndex()
-        }
-    }
-
     function selectTab(tabIndex) {
         ConversationsAdapter.currentTypeFilter =
                 (tabIndex === SidePanelTabBar.Conversations) ?
@@ -60,28 +47,25 @@ TabBar {
                     Profile.Type.PENDING
     }
 
-    visible: tabBarVisible
-
-    currentIndex: 0
-
     FilterTabButton {
-        id: pageOne
+        id: conversationsTabButton
 
+        down: currentTypeFilter !==  Profile.Type.PENDING
         tabBar: parent
-        down: true
         labelText: JamiStrings.conversations
         onSelected: selectTab(SidePanelTabBar.Conversations)
-        badgeCount: totalUnreadMessagesCount
+        badgeCount: ConversationsAdapter.totalUnreadMessageCount
         acceleratorSequence: "Ctrl+L"
     }
 
     FilterTabButton {
-        id: pageTwo
+        id: requestsTabButton
 
+        down: !conversationsTabButton.down
         tabBar: parent
         labelText: JamiStrings.invitations
         onSelected: selectTab(SidePanelTabBar.Requests)
-        badgeCount: pendingRequestCount
+        badgeCount: ConversationsAdapter.pendingRequestCount
         acceleratorSequence: "Ctrl+R"
     }
 }
diff --git a/src/mainview/components/SmartListItemDelegate.qml b/src/mainview/components/SmartListItemDelegate.qml
new file mode 100644
index 0000000000000000000000000000000000000000..fcabcf49995a9f35f449d66711f368a57c32f647
--- /dev/null
+++ b/src/mainview/components/SmartListItemDelegate.qml
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2020-2021 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@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 <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+import QtQuick.Layouts 1.14
+
+import net.jami.Models 1.0
+import net.jami.Adapters 1.0
+import net.jami.Constants 1.0
+
+import "../../commoncomponents"
+
+ItemDelegate {
+    id: root
+
+    width: ListView.view.width
+    height: JamiTheme.smartListItemHeight
+
+    function convUid() {
+        return UID
+    }
+
+    Component.onCompleted: {
+        if (ContactType === Profile.Type.TEMPORARY)
+            root.ListView.view.model.updateContactAvatarUid(URI)
+        avatar.updateImage(URI, PictureUid)
+    }
+
+    RowLayout {
+        anchors.fill: parent
+        anchors.leftMargin: 15
+        anchors.rightMargin: 15
+        spacing: 10
+
+        AvatarImage {
+            id: avatar
+
+            Connections {
+                target: root.ListView.view.model
+                function onDataChanged(index) {
+                    var model = root.ListView.view.model
+                    avatar.updateImage(URI === undefined ?
+                                           model.data(index, ConversationList.URI):
+                                           URI,
+                                       PictureUid === undefined ?
+                                           model.data(index, ConversationList.PictureUid):
+                                           PictureUid)
+                }
+            }
+
+            Layout.preferredWidth: JamiTheme.smartListAvatarSize
+            Layout.preferredHeight: JamiTheme.smartListAvatarSize
+
+            mode: AvatarImage.Mode.FromContactUri
+            showPresenceIndicator: Presence === undefined ? false : Presence
+            transitionDuration: 0
+        }
+
+        ColumnLayout {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            spacing: 0
+            // best name
+            Text {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 20
+                Layout.alignment: Qt.AlignVCenter
+                elide: Text.ElideRight
+                text: BestName === undefined ? "" : BestName
+                font.pointSize: JamiTheme.smartlistItemFontSize
+                font.weight: UnreadMessagesCount ? Font.Bold : Font.Normal
+                color: JamiTheme.textColor
+            }
+            RowLayout {
+                visible: ContactType !== Profile.Type.TEMPORARY
+                         && LastInteractionDate !== undefined
+                Layout.fillWidth: true
+                Layout.preferredHeight: 20
+                Layout.alignment: Qt.AlignTop
+                // last Interaction date
+                Text {
+                    Layout.alignment: Qt.AlignVCenter
+                    text: LastInteractionDate === undefined ? "" : LastInteractionDate
+                    font.pointSize: JamiTheme.smartlistItemInfoFontSize
+                    font.weight: UnreadMessagesCount ? Font.DemiBold : Font.Normal
+                    color: JamiTheme.textColor
+                }
+                // last Interaction
+                Text {
+                    elide: Text.ElideRight
+                    Layout.fillWidth: true
+                    Layout.alignment: Qt.AlignVCenter
+                    text: Draft ?
+                              Draft :
+                              (LastInteraction === undefined ? "" : LastInteraction)
+                    font.pointSize: JamiTheme.smartlistItemInfoFontSize
+                    font.weight: UnreadMessagesCount ? Font.Normal : Font.Light
+                    font.hintingPreference: Font.PreferNoHinting
+                    maximumLineCount: 1
+                    color: JamiTheme.textColor
+                    // deal with poor rendering of the pencil emoji on Windows
+                    font.family: Qt.platform.os === "windows" && Draft ?
+                                     "Segoe UI Emoji" :
+                                     Qt.application.font.family
+                    lineHeight: font.family === "Segoe UI Emoji" ? 1.25 : 1
+                }
+            }
+        }
+
+        ColumnLayout {
+            visible: InCall || UnreadMessagesCount
+            Layout.preferredWidth: childrenRect.width
+            Layout.fillHeight: true
+            spacing: 2
+            // call status
+            Text {
+                Layout.preferredHeight: 20
+                Layout.alignment: Qt.AlignRight
+                text: InCall ? UtilsAdapter.getCallStatusStr(CallState) : ""
+                font.pointSize: JamiTheme.smartlistItemInfoFontSize
+                font.weight: Font.Medium
+                color: JamiTheme.textColor
+            }
+            // unread message count
+            Item {
+                Layout.preferredWidth: childrenRect.width
+                Layout.preferredHeight: childrenRect.height
+                Layout.alignment: Qt.AlignTop | Qt.AlignRight
+                BadgeNotifier {
+                    size: 20
+                    count: UnreadMessagesCount
+                    animate: index === 0
+                }
+            }
+        }
+    }
+
+    background: Rectangle {
+        color: {
+            if (root.pressed)
+                return Qt.darker(JamiTheme.selectedColor, 1.1)
+            else if (root.hovered)
+                return Qt.darker(JamiTheme.selectedColor, 1.05)
+            else
+                return "transparent"
+        }
+    }
+
+    onClicked: ListView.view.model.select(index)
+    onDoubleClicked: {
+        ListView.view.model.select(index)
+        if (AccountAdapter.currentAccountType === Profile.Type.SIP)
+            CallAdapter.placeAudioOnlyCall()
+        else
+            CallAdapter.placeCall()
+
+        // TODO: factor this out (visible should be observing)
+        communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
+    }
+    onPressAndHold: ListView.view.openContextMenuAt(pressX, pressY, root)
+
+    MouseArea {
+        anchors.fill: parent
+        acceptedButtons: Qt.RightButton
+        onClicked: root.ListView.view.openContextMenuAt(mouse.x, mouse.y, root)
+    }
+}
diff --git a/src/messagesadapter.cpp b/src/messagesadapter.cpp
index 829b89a644454f7025075c27af67430f1deada1f..ecc4a90b2df2c7c262cee9eb4e2ee11f766cc14b 100644
--- a/src/messagesadapter.cpp
+++ b/src/messagesadapter.cpp
@@ -153,8 +153,6 @@ MessagesAdapter::connectConversationModel()
                                Q_UNUSED(convUid);
                                removeInteraction(interactionId);
                            });
-
-    currentConversationModel->setFilter("");
 }
 
 void
@@ -169,7 +167,7 @@ MessagesAdapter::sendContactRequest()
 void
 MessagesAdapter::updateConversationForAddedContact()
 {
-    auto* convModel = lrcInstance_->getCurrentConversationModel();
+    auto convModel = lrcInstance_->getCurrentConversationModel();
     const auto& convInfo = lrcInstance_->getConversationFromConvUid(
         lrcInstance_->get_selectedConvUid());
 
@@ -193,7 +191,6 @@ MessagesAdapter::slotSendMessageContentSaved(const QString& content)
     auto restoredContent = lrcInstance_->getContentDraft(lrcInstance_->get_selectedConvUid(),
                                                          lrcInstance_->getCurrAccId());
     setSendMessageContent(restoredContent);
-    Q_EMIT needToUpdateSmartList();
 }
 
 void
@@ -202,7 +199,6 @@ MessagesAdapter::slotUpdateDraft(const QString& content)
     if (!LastConvUid_.isEmpty()) {
         lrcInstance_->setContentDraft(LastConvUid_, lrcInstance_->getCurrAccId(), content);
     }
-    Q_EMIT needToUpdateSmartList();
 }
 
 void
@@ -460,11 +456,9 @@ MessagesAdapter::setConversationProfileData(const lrc::api::conversation::Info&
     try {
         auto& contact = accInfo->contactModel->getContact(contactUri);
         auto bestName = accInfo->contactModel->bestNameForContact(contactUri);
-        setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING
-                          || contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY,
+        setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING,
                       bestName,
                       contactUri);
-
         if (!contact.profileInfo.avatar.isEmpty()) {
             setSenderImage(contactUri, contact.profileInfo.avatar);
         } else {
diff --git a/src/messagesadapter.h b/src/messagesadapter.h
index 8fdcbf8599ce477b23224577c7c335a2ca4f18c0..350ad1dd7dbbeac38ffcaceccbc5d5329aaeee52 100644
--- a/src/messagesadapter.h
+++ b/src/messagesadapter.h
@@ -92,7 +92,6 @@ protected:
     void contactIsComposing(const QString& convUid, const QString& contactUri, bool isComposing);
 
 Q_SIGNALS:
-    void needToUpdateSmartList();
     void contactBanned();
     void navigateToWelcomePageRequested();
     void invitationAccepted();
diff --git a/src/qmlregister.cpp b/src/qmlregister.cpp
index b5bb3390486643ae0c494e3232380ccf1357c22d..862ae16ba06f7ff4f2831691fc8c3f3e2048c4ea 100644
--- a/src/qmlregister.cpp
+++ b/src/qmlregister.cpp
@@ -27,6 +27,7 @@
 #include "moderatorlistmodel.h"
 #include "deviceitemlistmodel.h"
 #include "smartlistmodel.h"
+#include "conversationlistmodelbase.h"
 
 #include "appsettingsmanager.h"
 #include "distantrenderer.h"
@@ -106,6 +107,10 @@ registerTypes()
     QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
     QML_REGISTERTYPE(NS_MODELS, SmartListModel);
 
+    // Roles & type enums for models
+    QML_REGISTERNAMESPACE(NS_MODELS, ConversationList::staticMetaObject, "ConversationList");
+    QML_REGISTERNAMESPACE(NS_MODELS, ContactList::staticMetaObject, "ContactList");
+
     // QQuickItems
     QML_REGISTERTYPE(NS_MODELS, PreviewRenderer);
     QML_REGISTERTYPE(NS_MODELS, VideoCallPreviewRenderer);
diff --git a/src/qmlregister.h b/src/qmlregister.h
index 896a2dc2f7ad5493ad60d896213de39de414dc44..b2a86842a31881cd4ea48e6f770063ecbcc00d6d 100644
--- a/src/qmlregister.h
+++ b/src/qmlregister.h
@@ -42,13 +42,12 @@ Q_CLASSINFO("RegisterEnumClassesUnscoped", "false")
     QQmlEngine::setObjectOwnership(I, QQmlEngine::CppOwnership); \
     { using T = std::remove_reference<decltype(*I)>::type; \
     qmlRegisterSingletonType<T>(NS, VER_MAJ, VER_MIN, N, \
-                                [I](QQmlEngine*, QJSEngine*) -> QObject* { \
-                                    return I; }); }
+                                [i=I](QQmlEngine*, QJSEngine*) -> QObject* { \
+                                    return i; }); }
 
 #define QML_REGISTERSINGLETONTYPE_CUSTOM(NS, T, P) \
     qmlRegisterSingletonType<T>(NS, VER_MAJ, VER_MIN, #T, \
-                                [p=P](QQmlEngine* e, QJSEngine* se) -> QObject* { \
-                                    Q_UNUSED(e); Q_UNUSED(se); \
+                                [p=P](QQmlEngine*, QJSEngine*) -> QObject* { \
                                     return p; \
                                 });
 // clang-format on
diff --git a/src/searchresultslistmodel.cpp b/src/searchresultslistmodel.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8329524a99fe89858ed2d881f90b49256832c3f4
--- /dev/null
+++ b/src/searchresultslistmodel.cpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk <andreas.traczyk@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 "searchresultslistmodel.h"
+
+SearchResultsListModel::SearchResultsListModel(LRCInstance* instance, QObject* parent)
+    : ConversationListModelBase(instance, parent)
+{}
+
+int
+SearchResultsListModel::rowCount(const QModelIndex& parent) const
+{
+    // For list models only the root node (an invalid parent) should return the list's size. For all
+    // other (valid) parents, rowCount() should return 0 so that it does not become a tree model.
+    if (!parent.isValid() && model_) {
+        return model_->getAllSearchResults().size();
+    }
+    return 0;
+}
+
+QVariant
+SearchResultsListModel::data(const QModelIndex& index, int role) const
+{
+    const auto& data = model_->getAllSearchResults();
+    if (!index.isValid() || data.empty())
+        return {};
+    return dataForItem(data.at(index.row()), role);
+}
+
+void
+SearchResultsListModel::setFilter(const QString& filterString)
+{
+    model_->setFilter(filterString);
+}
+
+void
+SearchResultsListModel::onSearchResultsUpdated()
+{
+    beginResetModel();
+    fillContactAvatarUidMap(lrcInstance_->getCurrentAccountInfo().contactModel->getAllContacts());
+    endResetModel();
+}
diff --git a/src/searchresultslistmodel.h b/src/searchresultslistmodel.h
new file mode 100644
index 0000000000000000000000000000000000000000..372302b303bf6d9bd9841256080b8fd72ac363ee
--- /dev/null
+++ b/src/searchresultslistmodel.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk <andreas.traczyk@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/>.
+ */
+
+#pragma once
+
+#include "conversationlistmodelbase.h"
+#include "selectablelistproxymodel.h"
+
+// A wrapper view model around ConversationModel's search result data
+class SearchResultsListModel : public ConversationListModelBase
+{
+    Q_OBJECT
+
+public:
+    explicit SearchResultsListModel(LRCInstance* instance, QObject* parent = nullptr);
+
+    int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+    virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+
+    Q_INVOKABLE void setFilter(const QString& filterString);
+
+public Q_SLOTS:
+    void onSearchResultsUpdated();
+};
+
+// The top level pre sorted and filtered model to be consumed by QML ListViews
+class SearchResultsListProxyModel final : public SelectableListProxyModel
+{
+    Q_OBJECT
+
+public:
+    explicit SearchResultsListProxyModel(QAbstractListModel* model, QObject* parent = nullptr)
+        : SelectableListProxyModel(model, parent) {};
+};
diff --git a/src/selectablelistproxymodel.cpp b/src/selectablelistproxymodel.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b0bfab1dbbbbce8677e9f00023fa644a696c64a3
--- /dev/null
+++ b/src/selectablelistproxymodel.cpp
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk <andreas.traczyk@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 "selectablelistproxymodel.h"
+
+SelectableListProxyModel::SelectableListProxyModel(QAbstractListModel* model, QObject* parent)
+    : QSortFilterProxyModel(parent)
+    , currentFilteredRow_(-1)
+    , selectedSourceIndex_(QModelIndex())
+{
+    bindSourceModel(model);
+}
+
+void
+SelectableListProxyModel::bindSourceModel(QAbstractListModel* model)
+{
+    setSourceModel(model);
+    connect(sourceModel(),
+            &QAbstractListModel::dataChanged,
+            this,
+            &SelectableListProxyModel::updateSelection,
+            Qt::UniqueConnection);
+    connect(model,
+            &QAbstractListModel::rowsInserted,
+            this,
+            &SelectableListProxyModel::updateSelection,
+            Qt::UniqueConnection);
+    connect(model,
+            &QAbstractListModel::rowsRemoved,
+            this,
+            &SelectableListProxyModel::updateSelection,
+            Qt::UniqueConnection);
+    connect(sourceModel(),
+            &QAbstractListModel::modelReset,
+            this,
+            &SelectableListProxyModel::deselect,
+            Qt::UniqueConnection);
+}
+
+void
+SelectableListProxyModel::setFilter(const QString& filterString)
+{
+    setFilterFixedString(filterString);
+    updateSelection();
+}
+
+void
+SelectableListProxyModel::select(const QModelIndex& index)
+{
+    selectedSourceIndex_ = mapToSource(index);
+    updateSelection();
+}
+
+void
+SelectableListProxyModel::select(int row)
+{
+    select(index(row, 0));
+}
+
+void
+SelectableListProxyModel::deselect()
+{
+    selectedSourceIndex_ = QModelIndex();
+    currentFilteredRow_ = -1;
+    Q_EMIT currentFilteredRowChanged();
+}
+
+QVariant
+SelectableListProxyModel::dataForRow(int row, int role) const
+{
+    return data(index(row, 0), role);
+}
+
+void
+SelectableListProxyModel::selectSourceRow(int row)
+{
+    // note: the convId <-> index binding loop present
+    // is broken here
+    if (row == -1 || selectedSourceIndex_.row() == row)
+        return;
+    selectedSourceIndex_ = sourceModel()->index(row, 0);
+    updateSelection();
+}
+
+void
+SelectableListProxyModel::updateContactAvatarUid(const QString& contactUri)
+{
+    auto base = qobject_cast<ConversationListModelBase*>(sourceModel());
+    if (base)
+        base->updateContactAvatarUid(contactUri);
+}
+
+void
+SelectableListProxyModel::updateSelection()
+{
+    // if there has been no valid selection made, there is
+    // nothing to update
+    if (!selectedSourceIndex_.isValid() && currentFilteredRow_ == -1)
+        return;
+
+    auto lastFilteredRow = currentFilteredRow_;
+    auto filteredIndex = mapFromSource(selectedSourceIndex_);
+
+    // if the source model is empty, invalidate the selection
+    if (sourceModel()->rowCount() == 0) {
+        set_currentFilteredRow(-1);
+        Q_EMIT validSelectionChanged();
+        return;
+    }
+
+    // if the source and filtered index is no longer valid
+    // this would indicate that a mutation has occured,
+    // thus any arbritrary ux decision is okay here
+    if (!selectedSourceIndex_.isValid()) {
+        auto row = qMax(--currentFilteredRow_, 0);
+        selectedSourceIndex_ = mapToSource(index(row, 0));
+        filteredIndex = mapFromSource(selectedSourceIndex_);
+        currentFilteredRow_ = filteredIndex.row();
+        Q_EMIT currentFilteredRowChanged();
+        Q_EMIT validSelectionChanged();
+        return;
+    }
+
+    // update the row for ListView observers
+    set_currentFilteredRow(filteredIndex.row());
+
+    // finally, if the filter index is invalid, then we have
+    // probably just filtered out the selected item and don't
+    // want to force reselection of other ui components, as the
+    // source index is still valid, in that case, or if the
+    // row hasn't changed, don't notify
+    if (filteredIndex.isValid() && lastFilteredRow != currentFilteredRow_) {
+        Q_EMIT validSelectionChanged();
+    }
+}
+
+SelectableListProxyGroupModel::SelectableListProxyGroupModel(QList<SelectableListProxyModel*> models,
+                                                             QObject* parent)
+    : QObject(parent)
+    , models_(models)
+{
+    Q_FOREACH (auto* m, models_) {
+        connect(m, &SelectableListProxyModel::validSelectionChanged, [this, m] {
+            // deselct all other lists in the group
+            Q_FOREACH (auto* otherM, models_) {
+                if (m != otherM) {
+                    otherM->deselect();
+                }
+            }
+        });
+    }
+}
diff --git a/src/selectablelistproxymodel.h b/src/selectablelistproxymodel.h
new file mode 100644
index 0000000000000000000000000000000000000000..af5b529fa4fbbcdb82b314d46193e2ffd6004771
--- /dev/null
+++ b/src/selectablelistproxymodel.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Andreas Traczyk <andreas.traczyk@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/>.
+ */
+
+#pragma once
+
+#include "conversationlistmodelbase.h"
+
+#include <QSortFilterProxyModel>
+
+// The base class for a filtered and sorted model.
+// The model may be part of a group and if so, will track a
+// mutually exclusive selection.
+class SelectableListProxyModel : public QSortFilterProxyModel
+{
+    Q_OBJECT
+    QML_PROPERTY(int, currentFilteredRow)
+
+public:
+    explicit SelectableListProxyModel(QAbstractListModel* model, QObject* parent = nullptr);
+
+    void bindSourceModel(QAbstractListModel* model);
+
+    Q_INVOKABLE void setFilter(const QString& filterString);
+    Q_INVOKABLE void select(const QModelIndex& index);
+    Q_INVOKABLE void select(int row);
+    Q_INVOKABLE void deselect();
+    Q_INVOKABLE QVariant dataForRow(int row, int role) const;
+    void selectSourceRow(int row);
+
+    // this may not be the best place for this but it prevents a level of
+    // inheritance and prevents code duplication
+    Q_INVOKABLE void updateContactAvatarUid(const QString& contactUri);
+
+public Q_SLOTS:
+    void updateSelection();
+
+Q_SIGNALS:
+    void validSelectionChanged();
+
+private:
+    QPersistentModelIndex selectedSourceIndex_;
+};
+
+class SelectableListProxyGroupModel : public QObject
+{
+    Q_OBJECT
+public:
+    explicit SelectableListProxyGroupModel(QList<SelectableListProxyModel*> models,
+                                           QObject* parent = nullptr);
+    QList<SelectableListProxyModel*> models_;
+};
diff --git a/src/settingsview/components/AdvancedCallSettings.qml b/src/settingsview/components/AdvancedCallSettings.qml
index fb567b14588916010f80e2db3fb8ff24a1702a6e..239421ff7a8d3631b437028688e292997196d095 100644
--- a/src/settingsview/components/AdvancedCallSettings.qml
+++ b/src/settingsview/components/AdvancedCallSettings.qml
@@ -232,7 +232,7 @@ ColumnLayout {
 
             onClicked: {
                 ContactPickerCreation.createContactPickerObjects(
-                            ContactPicker.ContactPickerType.CONVERSATION,
+                            ContactList.CONVERSATION,
                             mainView)
                 ContactPickerCreation.openContactPicker()
             }
diff --git a/src/settingsview/components/CurrentAccountSettings.qml b/src/settingsview/components/CurrentAccountSettings.qml
index 457814ad3bf937d074eb5121d6cb39210e4fcbd3..144efab6f746340be7f9554966590eba821f76ce 100644
--- a/src/settingsview/components/CurrentAccountSettings.qml
+++ b/src/settingsview/components/CurrentAccountSettings.qml
@@ -113,7 +113,7 @@ Rectangle {
         id: deleteAccountDialog
 
         onAccepted: {
-            AccountAdapter.setSelectedConvId()
+            LRCInstance.deselectConversation()
 
             if(UtilsAdapter.getAccountListSize() > 0) {
                 navigateToMainView()
diff --git a/src/smartlistmodel.cpp b/src/smartlistmodel.cpp
index 6269f43dd853d0d9007aec4f32b0aa72b92b9061..805218ff55d9ce5051cf3c1fb96ee4d3582a7a8e 100644
--- a/src/smartlistmodel.cpp
+++ b/src/smartlistmodel.cpp
@@ -1,4 +1,4 @@
-/*!
+/*
  * Copyright (C) 2017-2020 by Savoir-faire Linux
  * Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
@@ -34,17 +34,14 @@
 SmartListModel::SmartListModel(QObject* parent,
                                SmartListModel::Type listModelType,
                                LRCInstance* instance)
-    : AbstractListModelBase(parent)
+    : ConversationListModelBase(instance, parent)
     , listModelType_(listModelType)
 {
-    lrcInstance_ = instance;
     if (listModelType_ == Type::CONFERENCE) {
         setConferenceableFilter();
     }
 }
 
-SmartListModel::~SmartListModel() {}
-
 int
 SmartListModel::rowCount(const QModelIndex& parent) const
 {
@@ -70,102 +67,75 @@ SmartListModel::rowCount(const QModelIndex& parent) const
     return 0;
 }
 
-int
-SmartListModel::columnCount(const QModelIndex& parent) const
-{
-    Q_UNUSED(parent);
-    return 1;
-}
-
 QVariant
 SmartListModel::data(const QModelIndex& index, int role) const
 {
-    if (!index.isValid()) {
-        return QVariant();
-    }
-
-    try {
-        auto& currentAccountInfo = lrcInstance_->accountModel().getAccountInfo(
-            lrcInstance_->getCurrAccId());
-        auto& convModel = currentAccountInfo.conversationModel;
-        if (listModelType_ == Type::TRANSFER) {
+    if (!index.isValid())
+        return {};
+
+    switch (listModelType_) {
+    case Type::TRANSFER: {
+        try {
+            auto& currentAccountInfo = lrcInstance_->accountModel().getAccountInfo(
+                lrcInstance_->getCurrAccId());
+            auto& convModel = currentAccountInfo.conversationModel;
             auto filterType = currentAccountInfo.profileInfo.type;
             auto& item = convModel->getFilteredConversations(filterType).at(index.row());
-            return getConversationItemData(item, currentAccountInfo, role);
-        } else if (listModelType_ == Type::CONFERENCE) {
-            auto calls = conferenceables_[ConferenceableItem::CALL];
-            auto contacts = conferenceables_[ConferenceableItem::CONTACT];
-            QString itemConvUid {}, itemAccountId {};
-            if (calls.size() == 0) {
-                itemConvUid = contacts.at(index.row()).at(0).convId;
-                itemAccountId = contacts.at(index.row()).at(0).accountId;
-            } else {
-                bool callsOpen = sectionState_[tr("Calls")];
-                bool contactsOpen = sectionState_[tr("Contacts")];
-                auto callSectionEnd = callsOpen ? calls.size() + 1 : 1;
-                auto contactSectionEnd = contactsOpen ? callSectionEnd + contacts.size() + 1
-                                                      : callSectionEnd + 1;
-                if (index.row() < callSectionEnd) {
-                    if (index.row() == 0) {
-                        return QVariant(role == Role::SectionName
-                                            ? (callsOpen ? "➖ " : "➕ ") + QString(tr("Calls"))
-                                            : "");
-                    } else {
-                        auto idx = index.row() - 1;
-                        itemConvUid = calls.at(idx).at(0).convId;
-                        itemAccountId = calls.at(idx).at(0).accountId;
-                    }
-                } else if (index.row() < contactSectionEnd) {
-                    if (index.row() == callSectionEnd) {
-                        return QVariant(role == Role::SectionName
-                                            ? (contactsOpen ? "➖ " : "➕ ") + QString(tr("Contacts"))
-                                            : "");
-                    } else {
-                        auto idx = index.row() - (callSectionEnd + 1);
-                        itemConvUid = contacts.at(idx).at(0).convId;
-                        itemAccountId = contacts.at(idx).at(0).accountId;
-                    }
+            return dataForItem(item, role);
+        } catch (const std::exception& e) {
+            qWarning() << e.what();
+        }
+    } break;
+    case Type::CONFERENCE: {
+        auto calls = conferenceables_[ConferenceableItem::CALL];
+        auto contacts = conferenceables_[ConferenceableItem::CONTACT];
+        QString itemConvUid {}, itemAccountId {};
+        if (calls.size() == 0) {
+            itemConvUid = contacts.at(index.row()).at(0).convId;
+            itemAccountId = contacts.at(index.row()).at(0).accountId;
+        } else {
+            bool callsOpen = sectionState_[tr("Calls")];
+            bool contactsOpen = sectionState_[tr("Contacts")];
+            auto callSectionEnd = callsOpen ? calls.size() + 1 : 1;
+            auto contactSectionEnd = contactsOpen ? callSectionEnd + contacts.size() + 1
+                                                  : callSectionEnd + 1;
+            if (index.row() < callSectionEnd) {
+                if (index.row() == 0) {
+                    return QVariant(role == Role::SectionName
+                                        ? (callsOpen ? "➖ " : "➕ ") + QString(tr("Calls"))
+                                        : "");
+                } else {
+                    auto idx = index.row() - 1;
+                    itemConvUid = calls.at(idx).at(0).convId;
+                    itemAccountId = calls.at(idx).at(0).accountId;
+                }
+            } else if (index.row() < contactSectionEnd) {
+                if (index.row() == callSectionEnd) {
+                    return QVariant(role == Role::SectionName
+                                        ? (contactsOpen ? "➖ " : "➕ ") + QString(tr("Contacts"))
+                                        : "");
+                } else {
+                    auto idx = index.row() - (callSectionEnd + 1);
+                    itemConvUid = contacts.at(idx).at(0).convId;
+                    itemAccountId = contacts.at(idx).at(0).accountId;
                 }
             }
-            if (role == Role::AccountId) {
-                return QVariant(itemAccountId);
-            }
-
-            auto& itemAccountInfo = lrcInstance_->accountModel().getAccountInfo(itemAccountId);
-            auto& item = lrcInstance_->getConversationFromConvUid(itemConvUid, itemAccountId);
-            return getConversationItemData(item, itemAccountInfo, role);
-        } else if (listModelType_ == Type::CONVERSATION) {
-            auto& item = conversations_.at(index.row());
-            return getConversationItemData(item, currentAccountInfo, role);
         }
-    } catch (const std::exception& e) {
-        qWarning() << e.what();
-    }
-    return QVariant();
-}
+        if (role == Role::AccountId) {
+            return QVariant(itemAccountId);
+        }
 
-QHash<int, QByteArray>
-SmartListModel::roleNames() const
-{
-    QHash<int, QByteArray> roles;
-    roles[DisplayName] = "DisplayName";
-    roles[DisplayID] = "DisplayID";
-    roles[Presence] = "Presence";
-    roles[URI] = "URI";
-    roles[UnreadMessagesCount] = "UnreadMessagesCount";
-    roles[LastInteractionDate] = "LastInteractionDate";
-    roles[LastInteraction] = "LastInteraction";
-    roles[ContactType] = "ContactType";
-    roles[UID] = "UID";
-    roles[InCall] = "InCall";
-    roles[IsAudioOnly] = "IsAudioOnly";
-    roles[CallStackViewShouldShow] = "CallStackViewShouldShow";
-    roles[CallState] = "CallState";
-    roles[SectionName] = "SectionName";
-    roles[AccountId] = "AccountId";
-    roles[Draft] = "Draft";
-    roles[PictureUid] = "PictureUid";
-    return roles;
+        auto& item = lrcInstance_->getConversationFromConvUid(itemConvUid, itemAccountId);
+        return dataForItem(item, role);
+    } break;
+    case Type::CONVERSATION: {
+        auto& item = conversations_.at(index.row());
+        return dataForItem(item, role);
+    } break;
+    default:
+        break;
+    }
+    return {};
 }
 
 void
@@ -194,39 +164,6 @@ SmartListModel::fillConversationsList()
     endResetModel();
 }
 
-void
-SmartListModel::updateContactAvatarUid(const QString& contactUri)
-{
-    contactAvatarUidMap_[contactUri] = Utils::generateUid();
-}
-
-void
-SmartListModel::fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts)
-{
-    if (contacts.size() == 0) {
-        contactAvatarUidMap_.clear();
-        return;
-    }
-
-    if (contactAvatarUidMap_.isEmpty() || contacts.size() != contactAvatarUidMap_.size()) {
-        bool useContacts = contacts.size() > contactAvatarUidMap_.size();
-        auto contactsKeyList = contacts.keys();
-        auto contactAvatarUidMapKeyList = contactAvatarUidMap_.keys();
-
-        for (int i = 0;
-             i < (useContacts ? contactsKeyList.size() : contactAvatarUidMapKeyList.size());
-             ++i) {
-            // Insert or update
-            if (i < contactsKeyList.size() && !contactAvatarUidMap_.contains(contactsKeyList.at(i)))
-                contactAvatarUidMap_.insert(contactsKeyList.at(i), Utils::generateUid());
-            // Remove
-            if (i < contactAvatarUidMapKeyList.size()
-                && !contacts.contains(contactAvatarUidMapKeyList.at(i)))
-                contactAvatarUidMap_.remove(contactAvatarUidMapKeyList.at(i));
-        }
-    }
-}
-
 void
 SmartListModel::toggleSection(const QString& section)
 {
@@ -251,140 +188,6 @@ SmartListModel::currentUidSmartListModelIndex()
     return -1;
 }
 
-QVariant
-SmartListModel::getConversationItemData(const conversation::Info& item,
-                                        const account::Info& accountInfo,
-                                        int role) const
-{
-    if (item.participants.size() <= 0) {
-        return QVariant();
-    }
-    auto& contactModel = accountInfo.contactModel;
-
-    // Since we are using image provider right now, image url representation should be unique to
-    // be able to use the image cache, account avatar will only be updated once PictureUid changed
-    switch (role) {
-    case Role::DisplayName: {
-        if (!item.participants.isEmpty())
-            return QVariant(contactModel->bestNameForContact(item.participants[0]));
-        return QVariant("");
-    }
-    case Role::DisplayID: {
-        if (!item.participants.isEmpty())
-            return QVariant(contactModel->bestIdForContact(item.participants[0]));
-        return QVariant("");
-    }
-    case Role::Presence: {
-        if (!item.participants.isEmpty()) {
-            auto& contact = contactModel->getContact(item.participants[0]);
-            return QVariant(contact.isPresent);
-        }
-        return QVariant(false);
-    }
-    case Role::PictureUid: {
-        if (!item.participants.isEmpty()) {
-            return QVariant(contactAvatarUidMap_[item.participants[0]]);
-        }
-        return QVariant("");
-    }
-    case Role::URI: {
-        if (!item.participants.isEmpty()) {
-            return QVariant(item.participants[0]);
-        }
-        return QVariant("");
-    }
-    case Role::UnreadMessagesCount:
-        return QVariant(item.unreadMessages);
-    case Role::LastInteractionDate: {
-        if (!item.interactions.empty()) {
-            auto& date = item.interactions.at(item.lastMessageUid).timestamp;
-            return QVariant(Utils::formatTimeString(date));
-        }
-        return QVariant("");
-    }
-    case Role::LastInteraction: {
-        if (!item.interactions.empty()) {
-            return QVariant(item.interactions.at(item.lastMessageUid).body);
-        }
-        return QVariant("");
-    }
-    case Role::LastInteractionType: {
-        if (!item.interactions.empty()) {
-            return QVariant(static_cast<int>(item.interactions.at(item.lastMessageUid).type));
-        }
-        return QVariant(0);
-    }
-    case Role::ContactType: {
-        if (!item.participants.isEmpty()) {
-            auto& contact = contactModel->getContact(item.participants[0]);
-            return QVariant(static_cast<int>(contact.profileInfo.type));
-        }
-        return QVariant(0);
-    }
-    case Role::UID:
-        return QVariant(item.uid);
-    case Role::InCall: {
-        const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
-        if (!convInfo.uid.isEmpty()) {
-            auto* callModel = lrcInstance_->getCurrentCallModel();
-            return QVariant(callModel->hasCall(convInfo.callId));
-        }
-        return QVariant(false);
-    }
-    case Role::IsAudioOnly: {
-        const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
-        if (!convInfo.uid.isEmpty()) {
-            auto* call = lrcInstance_->getCallInfoForConversation(convInfo);
-            if (call) {
-                return QVariant(call->isAudioOnly);
-            }
-        }
-        return QVariant();
-    }
-    case Role::CallStackViewShouldShow: {
-        const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
-        if (!convInfo.uid.isEmpty() && !convInfo.callId.isEmpty()) {
-            auto* callModel = lrcInstance_->getCurrentCallModel();
-            const auto& call = callModel->getCall(convInfo.callId);
-            return QVariant(
-                callModel->hasCall(convInfo.callId)
-                && ((!call.isOutgoing
-                     && (call.status == lrc::api::call::Status::IN_PROGRESS
-                         || call.status == lrc::api::call::Status::PAUSED
-                         || call.status == lrc::api::call::Status::INCOMING_RINGING))
-                    || (call.isOutgoing && call.status != lrc::api::call::Status::ENDED)));
-        }
-        return QVariant(false);
-    }
-    case Role::CallState: {
-        const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
-        if (!convInfo.uid.isEmpty()) {
-            if (auto* call = lrcInstance_->getCallInfoForConversation(convInfo)) {
-                return QVariant(static_cast<int>(call->status));
-            }
-        }
-        return QVariant();
-    }
-    case Role::SectionName:
-        return QVariant(QString());
-    case Role::Draft: {
-        if (!item.uid.isEmpty()) {
-            const auto draft = lrcInstance_->getContentDraft(item.uid, accountInfo.id);
-            if (!draft.isEmpty()) {
-                /*
-                 * Pencil Emoji
-                 */
-                uint cp = 0x270F;
-                auto emojiString = QString::fromUcs4(&cp, 1);
-                return emojiString + lrcInstance_->getContentDraft(item.uid, accountInfo.id);
-            }
-        }
-        return QVariant("");
-    }
-    }
-    return QVariant();
-}
-
 QModelIndex
 SmartListModel::index(int row, int column, const QModelIndex& parent) const
 {
@@ -399,13 +202,6 @@ SmartListModel::index(int row, int column, const QModelIndex& parent) const
     return QModelIndex();
 }
 
-QModelIndex
-SmartListModel::parent(const QModelIndex& child) const
-{
-    Q_UNUSED(child);
-    return QModelIndex();
-}
-
 Qt::ItemFlags
 SmartListModel::flags(const QModelIndex& index) const
 {
diff --git a/src/smartlistmodel.h b/src/smartlistmodel.h
index 708c745582c93bed43b69990b4e7d4cb31746ff0..f35a9e8d06719ddbd60bb0925567376d64b6f94c 100644
--- a/src/smartlistmodel.h
+++ b/src/smartlistmodel.h
@@ -20,60 +20,32 @@
 
 #pragma once
 
-#include "abstractlistmodelbase.h"
+#include "conversationlistmodelbase.h"
+
+namespace ContactList {
+Q_NAMESPACE
+enum Type { CONVERSATION, CONFERENCE, TRANSFER, COUNT__ };
+Q_ENUM_NS(Type)
+} // namespace ContactList
 
 using namespace lrc::api;
 class LRCInstance;
 
-class SmartListModel : public AbstractListModelBase
+class SmartListModel : public ConversationListModelBase
 {
     Q_OBJECT
 public:
-    using AccountInfo = lrc::api::account::Info;
-    using ConversationInfo = lrc::api::conversation::Info;
-    using ContactInfo = lrc::api::contact::Info;
-
-    enum class Type { CONVERSATION, CONFERENCE, TRANSFER, COUNT__ };
-
-    enum Role {
-        DisplayName = Qt::UserRole + 1,
-        DisplayID,
-        Presence,
-        URI,
-        UnreadMessagesCount,
-        LastInteractionDate,
-        LastInteraction,
-        LastInteractionType,
-        ContactType,
-        UID,
-        ContextMenuOpen,
-        InCall,
-        IsAudioOnly,
-        CallStackViewShouldShow,
-        CallState,
-        SectionName,
-        AccountId,
-        PictureUid,
-        Draft
-    };
-    Q_ENUM(Role)
+    using Type = ContactList::Type;
 
     explicit SmartListModel(QObject* parent = nullptr,
-                            SmartListModel::Type listModelType = Type::CONVERSATION,
+                            Type listModelType = Type::CONVERSATION,
                             LRCInstance* instance = nullptr);
-    ~SmartListModel();
 
-    /*
-     * QAbstractListModel.
-     */
     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;
-    QHash<int, QByteArray> roleNames() const override;
     QModelIndex index(int row,
                       int column = 0,
                       const QModelIndex& parent = QModelIndex()) const override;
-    QModelIndex parent(const QModelIndex& child) const override;
     Qt::ItemFlags flags(const QModelIndex& index) const override;
 
     Q_INVOKABLE void setConferenceableFilter(const QString& filter = {});
@@ -81,28 +53,9 @@ public:
     Q_INVOKABLE int currentUidSmartListModelIndex();
     Q_INVOKABLE void fillConversationsList();
 
-    /*
-     * This function is to update contact avatar uuid for current account when there's an contact
-     * avatar changed.
-     */
-    Q_INVOKABLE void updateContactAvatarUid(const QString& contactUri);
-
 private:
-    QVariant getConversationItemData(const ConversationInfo& item,
-                                     const AccountInfo& accountInfo,
-                                     int role) const;
-
-    /*
-     * Give a uuid for each contact avatar for current account and it will serve PictureUid role
-     */
-    void fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts);
-
-    /*
-     * List sectioning.
-     */
     Type listModelType_;
     QMap<QString, bool> sectionState_;
     QMap<ConferenceableItem, ConferenceableValue> conferenceables_;
-    QMap<QString, QString> contactAvatarUidMap_;
     ConversationModel::ConversationQueueProxy conversations_;
 };
diff --git a/src/utils.cpp b/src/utils.cpp
index 7c4c7d4ec890c8dee9f91a5aeac57dac91c099cb..69434abf55b92b34c81f5b2d9779cb2123a3caa8 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -501,9 +501,9 @@ Utils::formatTimeString(const std::time_t& timeStamp)
 {
     auto currentTimeStamp = QDateTime::fromSecsSinceEpoch(timeStamp);
     auto now = QDateTime::currentDateTime();
-    auto timeStampDMY = currentTimeStamp.toString("dd/MM/yyyy");
-    if (timeStampDMY == now.toString("dd/MM/yyyy")) {
-        return currentTimeStamp.toString("hh:mm");
+    auto timeStampDMY = currentTimeStamp.toString("dd/MM/yy");
+    if (timeStampDMY == now.toString("dd/MM/yy")) {
+        return currentTimeStamp.toString("hhmm");
     } else {
         return timeStampDMY;
     }
diff --git a/src/utilsadapter.cpp b/src/utilsadapter.cpp
index 84d900c5d2a9922fc931222b7231755921e4182e..75b78cc7c27f68bfccb16c6261bfe21c7eb00264 100644
--- a/src/utilsadapter.cpp
+++ b/src/utilsadapter.cpp
@@ -144,29 +144,6 @@ UtilsAdapter::getBestId(const QString& accountId, const QString& uid)
     return QString();
 }
 
-int
-UtilsAdapter::getTotalUnreadMessages()
-{
-    int totalUnreadMessages {0};
-    if (lrcInstance_->getCurrentAccountInfo().profileInfo.type != lrc::api::profile::Type::SIP) {
-        auto* convModel = lrcInstance_->getCurrentConversationModel();
-        auto ringConversations = convModel->getFilteredConversations(lrc::api::profile::Type::RING,
-                                                                     false);
-        ringConversations.for_each(
-            [&totalUnreadMessages](const lrc::api::conversation::Info& conversation) {
-                totalUnreadMessages += conversation.unreadMessages;
-            });
-    }
-    return totalUnreadMessages;
-}
-
-int
-UtilsAdapter::getTotalPendingRequest()
-{
-    auto& accountInfo = lrcInstance_->getCurrentAccountInfo();
-    return accountInfo.contactModel->pendingRequestCount();
-}
-
 void
 UtilsAdapter::setConversationFilter(const QString& filter)
 {
diff --git a/src/utilsadapter.h b/src/utilsadapter.h
index 50909b90487bb609534ae0d0fe6124c3360583ba..6fd23d3799ccb77e7ea14deecc246b18c2d5af22 100644
--- a/src/utilsadapter.h
+++ b/src/utilsadapter.h
@@ -49,8 +49,6 @@ public:
     Q_INVOKABLE QString GetRingtonePath();
     Q_INVOKABLE bool checkStartupLink();
     Q_INVOKABLE void setConversationFilter(const QString& filter);
-    Q_INVOKABLE int getTotalUnreadMessages();
-    Q_INVOKABLE int getTotalPendingRequest();
     Q_INVOKABLE const QString getBestName(const QString& accountId, const QString& uid);
     Q_INVOKABLE const QString getPeerUri(const QString& accountId, const QString& uid);
     Q_INVOKABLE QString getBestId(const QString& accountId);