diff --git a/src/app/calladapter.cpp b/src/app/calladapter.cpp
index ddc608d7a228fb581fb1e88d7e7c6d9ed454d141..58862ef6104b132c96bbb8089ba0ff3d880afe0d 100644
--- a/src/app/calladapter.cpp
+++ b/src/app/calladapter.cpp
@@ -33,6 +33,7 @@
 #include <QJsonObject>
 
 #include <api/callparticipantsmodel.h>
+#include <api/devicemodel.h>
 
 #include <media_const.h>
 
@@ -42,16 +43,7 @@ CallAdapter::CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject*
 {
     participantsModel_.reset(new CallParticipantsModel(lrcInstance_, this));
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, participantsModel_.get(), "CallParticipantsModel");
-    participantsModelFiltered_.reset(
-        new GenericParticipantsFilterModel(lrcInstance_, participantsModel_.get()));
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS,
-                                      participantsModelFiltered_.get(),
-                                      "GenericParticipantsFilterModel");
-    activeParticipantsModel_.reset(
-        new ActiveParticipantsFilterModel(lrcInstance_, participantsModel_.get()));
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS,
-                                      activeParticipantsModel_.get(),
-                                      "ActiveParticipantsFilterModel");
+
     overlayModel_.reset(new CallOverlayModel(lrcInstance_, this));
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, overlayModel_.get(), "CallOverlayModel");
 
@@ -517,18 +509,35 @@ CallAdapter::updateCall(const QString& convUid, const QString& accountId, bool f
 void
 CallAdapter::fillParticipantData(QJsonObject& participant) const
 {
-    participant[lrc::api::ParticipantsInfosStrings::BESTNAME]
-        = participant[lrc::api::ParticipantsInfosStrings::URI];
+    // TODO: getCurrentDeviceId should be part of CurrentAccount, and Current<thing>
+    //       should be read accessible through LRCInstance ??
+    auto getCurrentDeviceId = [](const account::Info& accInfo) -> QString {
+        const auto& deviceList = accInfo.deviceModel->getAllDevices();
+        auto devIt = std::find_if(std::cbegin(deviceList),
+                                  std::cend(deviceList),
+                                  [](const Device& dev) { return dev.isCurrent; });
+        return devIt != deviceList.cend() ? devIt->id : QString();
+    };
+
     auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_);
-    participant[lrc::api::ParticipantsInfosStrings::ISLOCAL] = false;
-    if (participant[lrc::api::ParticipantsInfosStrings::BESTNAME] == accInfo.profileInfo.uri) {
-        participant[lrc::api::ParticipantsInfosStrings::BESTNAME] = tr("me");
-        participant[lrc::api::ParticipantsInfosStrings::ISLOCAL] = true;
+    using namespace lrc::api::ParticipantsInfosStrings;
+
+    // If both the URI and the device Id match, we set the "is local" flag
+    // used to filter out the participant.
+    // TODO:
+    //  - This filter should always be applied, and any local streams should render
+    //    using local sinks. Local non-preview participants should have proxy participant
+    //    items replaced into this model using their local sink Ids.
+    //  - The app setting should remain and be used to control whether or not the preview
+    //    sink partipcant is rendered.
+    auto uri = participant[URI].toString();
+    participant[ISLOCAL] = false;
+    if (uri == accInfo.profileInfo.uri && participant[DEVICE] == getCurrentDeviceId(accInfo)) {
+        participant[BESTNAME] = tr("me");
+        participant[ISLOCAL] = true;
     } else {
         try {
-            participant[lrc::api::ParticipantsInfosStrings::BESTNAME]
-                = lrcInstance_->getCurrentAccountInfo().contactModel->bestNameForContact(
-                    participant[lrc::api::ParticipantsInfosStrings::URI].toString());
+            participant[BESTNAME] = accInfo.contactModel->bestNameForContact(uri);
         } catch (...) {
         }
     }
diff --git a/src/app/calladapter.h b/src/app/calladapter.h
index 254ca3b84f41bda65b62080625b09966d12cf3cd..0e18614b5f531c30a292fcdd9a8a8ec8ddfcbba3 100644
--- a/src/app/calladapter.h
+++ b/src/app/calladapter.h
@@ -132,7 +132,5 @@ private:
     SystemTray* systemTray_;
     QScopedPointer<CallOverlayModel> overlayModel_;
     QScopedPointer<CallParticipantsModel> participantsModel_;
-    QScopedPointer<GenericParticipantsFilterModel> participantsModelFiltered_;
-    QScopedPointer<ActiveParticipantsFilterModel> activeParticipantsModel_;
     VectorString currentConfSubcalls_;
 };
diff --git a/src/app/callparticipantsmodel.cpp b/src/app/callparticipantsmodel.cpp
index 1fd8ace59b2b25a674b2205cf056195b3fb153b0..48a8544febaa200ac654c8956a6c3630c9728e04 100644
--- a/src/app/callparticipantsmodel.cpp
+++ b/src/app/callparticipantsmodel.cpp
@@ -51,56 +51,42 @@ CallParticipantsModel::data(const QModelIndex& index, int role) const
 
     using namespace CallParticipant;
     // Internal call, so no need to protect participants_ as locked higher
-    auto participant = participants_.at(index.row());
+    auto& item = participants_.at(index.row()).item;
 
+    using namespace ParticipantsInfosStrings;
     switch (role) {
     case Role::Uri:
-        return QVariant::fromValue(participant.item.value(lrc::api::ParticipantsInfosStrings::URI));
+        return QVariant(item.value(URI).toString());
     case Role::BestName:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::BESTNAME));
+        return QVariant(item.value(BESTNAME).toString());
     case Role::Device:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::DEVICE));
+        return QVariant(item.value(DEVICE).toString());
     case Role::Active:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::ACTIVE));
+        return QVariant(item.value(ACTIVE).toBool());
     case Role::AudioLocalMuted:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::AUDIOLOCALMUTED));
+        return QVariant(item.value(AUDIOLOCALMUTED).toBool());
     case Role::AudioModeratorMuted:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::AUDIOMODERATORMUTED));
+        return QVariant(item.value(AUDIOMODERATORMUTED).toBool());
     case Role::VideoMuted:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::VIDEOMUTED));
+        return QVariant(item.value(VIDEOMUTED).toBool());
     case Role::IsModerator:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::ISMODERATOR));
+        return QVariant(item.value(ISMODERATOR).toBool());
     case Role::IsLocal:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::ISLOCAL));
+        return QVariant(item.value(ISLOCAL).toBool());
     case Role::IsContact:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::ISCONTACT));
+        return QVariant(item.value(ISCONTACT).toBool());
     case Role::Avatar:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::AVATAR));
+        return QVariant(item.value(AVATAR).toString());
     case Role::SinkId:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::STREAMID));
+        return QVariant(item.value(STREAMID).toString());
     case Role::Height:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::HEIGHT));
+        return QVariant(item.value(HEIGHT).toInt());
     case Role::Width:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::WIDTH));
+        return QVariant(item.value(WIDTH).toInt());
     case Role::HandRaised:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::HANDRAISED));
+        return QVariant(item.value(HANDRAISED).toBool());
     case Role::VoiceActivity:
-        return QVariant::fromValue(
-            participant.item.value(lrc::api::ParticipantsInfosStrings::VOICEACTIVITY));
+        return QVariant(item.value(VOICEACTIVITY).toBool());
     }
     return QVariant();
 }
@@ -124,11 +110,13 @@ CallParticipantsModel::addParticipant(int index, const QVariant& infos)
         return;
     beginInsertRows(QModelIndex(), index, index);
 
-    auto it = participants_.begin() + index;
+    const auto it = participants_.constBegin() + index;
     participants_.insert(it, CallParticipant::Item {infos.toJsonObject()});
 
     endInsertRows();
 
+    set_count(rowCount());
+
     callId_ = participants_[index].item[lrc::api::ParticipantsInfosStrings::CALLID].toString();
 }
 
@@ -157,10 +145,12 @@ CallParticipantsModel::removeParticipant(int index)
 
     beginRemoveRows(QModelIndex(), index, index);
 
-    auto it = participants_.begin() + index;
+    const auto it = participants_.constBegin() + index;
     participants_.erase(it);
 
     endRemoveRows();
+
+    set_count(rowCount());
 }
 
 void
@@ -176,12 +166,14 @@ CallParticipantsModel::setParticipants(const QString& callId, const QVariantList
         int idx = 0;
         for (const auto& participant : participants) {
             beginInsertRows(QModelIndex(), idx, idx);
-            auto it = participants_.begin() + idx;
+            const auto it = participants_.constBegin() + idx;
             participants_.insert(it, CallParticipant::Item {participant.toJsonObject()});
             endInsertRows();
             idx++;
         }
     }
+
+    set_count(rowCount());
 }
 
 void
@@ -189,4 +181,6 @@ CallParticipantsModel::reset()
 {
     beginResetModel();
     endResetModel();
+
+    set_count(rowCount());
 }
diff --git a/src/app/callparticipantsmodel.h b/src/app/callparticipantsmodel.h
index bc485fb66a5e09785284ad0c218dc4024dba48cc..15330360c45830221c4af57eacb7da0874b4ee38 100644
--- a/src/app/callparticipantsmodel.h
+++ b/src/app/callparticipantsmodel.h
@@ -69,92 +69,13 @@ struct Item
 };
 } // namespace CallParticipant
 
-/*
- * The CurrentAccountFilterModel class
- * is for the sole purpose of filtering out current account.
- */
-class GenericParticipantsFilterModel final : public QSortFilterProxyModel
-{
-    Q_OBJECT
-    QML_PROPERTY(bool, hideSelf)
-    QML_PROPERTY(bool, hideAudioOnly)
-
-public:
-    explicit GenericParticipantsFilterModel(LRCInstance* lrcInstance,
-                                            QAbstractListModel* parent = nullptr)
-        : QSortFilterProxyModel(parent)
-        , lrcInstance_(lrcInstance)
-    {
-        setSourceModel(parent);
-        setFilterRole(CallParticipant::Role::Active);
-    }
-
-    virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
-    {
-        // Accept all participants in participants list filtered with active status.
-        auto index = sourceModel()->index(sourceRow, 0, sourceParent);
-
-        bool acceptState = !sourceModel()->data(index, CallParticipant::Role::Active).toBool();
-        bool importantState = sourceModel()->data(index, CallParticipant::Role::HandRaised).toBool()
-                              || sourceModel()->data(index, CallParticipant::Role::VoiceActivity).toBool();
-        if (acceptState && !importantState
-            && ((hideSelf_ && sourceModel()->rowCount() > 1 && sourceModel()->data(index, CallParticipant::Role::IsLocal).toBool())
-                || (hideAudioOnly_ && sourceModel()->rowCount() > 1 && sourceModel()->data(index, CallParticipant::Role::VideoMuted).toBool())))
-            acceptState = false;
-
-        return acceptState;
-    }
-
-    Q_INVOKABLE void reset()
-    {
-        beginResetModel();
-        endResetModel();
-    }
-
-protected:
-    LRCInstance* lrcInstance_ {nullptr};
-};
-
-/*
- * The ActiveParticipantsFilterModel class
- * is for the sole purpose of filtering out current account.
- */
-class ActiveParticipantsFilterModel final : public QSortFilterProxyModel
-{
-    Q_OBJECT
-
-public:
-    explicit ActiveParticipantsFilterModel(LRCInstance* lrcInstance,
-                                           QAbstractListModel* parent = nullptr)
-        : QSortFilterProxyModel(parent)
-        , lrcInstance_(lrcInstance)
-    {
-        setSourceModel(parent);
-        setFilterRole(CallParticipant::Role::Active);
-    }
-
-    virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
-    {
-        // Accept all participants in participants list filtered with active status.
-        auto index = sourceModel()->index(sourceRow, 0, sourceParent);
-        return sourceModel()->data(index, CallParticipant::Role::Active).toBool();
-    }
-
-    Q_INVOKABLE void reset()
-    {
-        beginResetModel();
-        endResetModel();
-    }
-
-protected:
-    LRCInstance* lrcInstance_ {nullptr};
-};
-
 class CallParticipantsModel : public QAbstractListModel
 {
     Q_OBJECT
 
     Q_PROPERTY(LayoutType conferenceLayout READ conferenceLayout NOTIFY layoutChanged)
+    QML_RO_PROPERTY(int, count)
+
 public:
     CallParticipantsModel(LRCInstance* instance, QObject* parent = nullptr);
 
@@ -170,6 +91,7 @@ public:
     void removeParticipant(int index);
     void setParticipants(const QString& callId, const QVariantList& participants);
     Q_INVOKABLE void reset();
+
     void setConferenceLayout(int layout, const QString& callId)
     {
         auto newLayout = static_cast<LayoutType>(layout);
@@ -178,13 +100,12 @@ public:
             Q_EMIT layoutChanged();
         }
     }
-    const LayoutType conferenceLayout()
+    LayoutType conferenceLayout()
     {
         return layout_;
     }
 
 Q_SIGNALS:
-    void updateParticipant(QVariant participantInfos);
     void layoutChanged();
 
 private:
diff --git a/src/app/currentconversation.h b/src/app/currentconversation.h
index 7ccbf511e63fa7d81c8f6b09b84a3aba4e4ab17f..37259baa4e56e12a75210e5f2478fc73f377f70c 100644
--- a/src/app/currentconversation.h
+++ b/src/app/currentconversation.h
@@ -51,6 +51,10 @@ class CurrentConversation final : public QObject
     QML_PROPERTY(QStringList, errors)
     QML_PROPERTY(QStringList, backendErrors)
 
+    // TODO: these belong in CurrentCall(which doesn't exist yet)
+    QML_PROPERTY(bool, hideSelf)
+    QML_PROPERTY(bool, hideAudioOnly)
+
 public:
     explicit CurrentConversation(LRCInstance* lrcInstance, QObject* parent = nullptr);
     ~CurrentConversation() = default;
diff --git a/src/app/mainview/components/CallActionBar.qml b/src/app/mainview/components/CallActionBar.qml
index 159ca617bb4e0bbba42ad570463fd370f3d561be..f70faf8c9e1693e0fed5d52a122a8b309fc85bf3 100644
--- a/src/app/mainview/components/CallActionBar.qml
+++ b/src/app/mainview/components/CallActionBar.qml
@@ -178,15 +178,11 @@ Control {
                         break
                   case JamiStrings.hideSelf:
                         UtilsAdapter.setAppValue(Settings.HideSelf, !layoutModel.get(index).ActiveSetting)
-                        GenericParticipantsFilterModel.hideSelf = UtilsAdapter.getAppValue(Settings.HideSelf)
-                        GenericParticipantsFilterModel.hideAudioOnly = UtilsAdapter.getAppValue(Settings.HideAudioOnly)
-                        GenericParticipantsFilterModel.reset()
+                        CurrentConversation.hideSelf = UtilsAdapter.getAppValue(Settings.HideSelf)
                         break
                   case JamiStrings.hideAudioOnly:
                         UtilsAdapter.setAppValue(Settings.HideAudioOnly, !layoutModel.get(index).ActiveSetting)
-                        GenericParticipantsFilterModel.hideSelf = UtilsAdapter.getAppValue(Settings.HideSelf)
-                        GenericParticipantsFilterModel.hideAudioOnly = UtilsAdapter.getAppValue(Settings.HideAudioOnly)
-                        GenericParticipantsFilterModel.reset()
+                        CurrentConversation.hideAudioOnly = UtilsAdapter.getAppValue(Settings.HideAudioOnly)
                         break
                 }
             }
diff --git a/src/app/mainview/components/ParticipantsLayer.qml b/src/app/mainview/components/ParticipantsLayer.qml
index b4d28eef3791d431c352c305c1e2d8af674ab586..2e5c8b78c00d6a4c0abde7e032c5314f62a7fa67 100644
--- a/src/app/mainview/components/ParticipantsLayer.qml
+++ b/src/app/mainview/components/ParticipantsLayer.qml
@@ -22,6 +22,8 @@ import QtQuick
 import QtQuick.Layouts
 import QtQuick.Controls
 
+import SortFilterProxyModel 0.2
+
 import net.jami.Adapters 1.1
 import net.jami.Models 1.1
 import net.jami.Constants 1.1
@@ -36,19 +38,8 @@ Item {
     property bool participantsSide
 
     onVisibleChanged: {
-        GenericParticipantsFilterModel.hideSelf = UtilsAdapter.getAppValue(Settings.HideSelf)
-        GenericParticipantsFilterModel.hideAudioOnly = UtilsAdapter.getAppValue(Settings.HideAudioOnly)
-    }
-
-    Connections {
-        target: GenericParticipantsFilterModel
-
-        function onHideSelfChanged() {
-            GenericParticipantsFilterModel.reset()
-        }
-        function onHideAudioOnlyChanged() {
-            GenericParticipantsFilterModel.reset()
-        }
+        CurrentConversation.hideSelf = UtilsAdapter.getAppValue(Settings.HideSelf)
+        CurrentConversation.hideAudioOnly = UtilsAdapter.getAppValue(Settings.HideAudioOnly)
     }
 
     Component {
@@ -84,7 +75,7 @@ Item {
                 enabled: bestName_ === uri_
 
                 function onRegisteredNameFound(status, address, name) {
-                    if (address === uri_ && status == NameDirectory.LookupStatus.SUCCESS) {
+                    if (address === uri_ && status === NameDirectory.LookupStatus.SUCCESS) {
                         bestName_ = name
                     }
                 }
@@ -92,11 +83,36 @@ Item {
         }
     }
 
+    SortFilterProxyModel {
+        id: genericParticipantsModel
+        sourceModel: CallParticipantsModel
+        filters: AllOf {
+            ValueFilter { roleName: "Active"; value: false }
+            ValueFilter {
+                enabled: CallParticipantsModel.count > 1 &&
+                         CurrentConversation.hideSelf
+                roleName: "IsLocal"
+                value: false
+            }
+            ValueFilter {
+                enabled: CallParticipantsModel.count > 1 &&
+                         CurrentConversation.hideAudioOnly
+                roleName: "VideoMuted"
+                value: false
+            }
+        }
+    }
+
+    SortFilterProxyModel {
+        id: activeParticipantsModel
+        sourceModel: CallParticipantsModel
+        filters: ValueFilter { roleName: "Active"; value: true }
+    }
+
     ParticipantsLayoutVertical {
         anchors.fill: parent
         participantComponent: callVideoMedia
         visible: !participantsSide
-
         onLayoutCountChanged: root.count = layoutCount
     }
 
diff --git a/src/app/mainview/components/ParticipantsLayoutHorizontal.qml b/src/app/mainview/components/ParticipantsLayoutHorizontal.qml
index 8177921c8d55c311e92213472807cbfa697bc0d3..285be952817fb9da740184d2303de0ae0764ce2b 100644
--- a/src/app/mainview/components/ParticipantsLayoutHorizontal.qml
+++ b/src/app/mainview/components/ParticipantsLayoutHorizontal.qml
@@ -19,8 +19,8 @@
 
 import QtQuick
 
-import QtQuick.Layouts 1.15
-import QtQuick.Controls 2.15
+import QtQuick.Layouts
+import QtQuick.Controls
 
 import net.jami.Adapters 1.1
 import net.jami.Models 1.1
@@ -96,7 +96,7 @@ SplitView {
             anchors.fill: parent
             anchors.centerIn: parent
 
-            model: ActiveParticipantsFilterModel
+            model: activeParticipantsModel
             delegate: Loader {
                 active: root.visible
                 asynchronous: true
@@ -262,7 +262,7 @@ SplitView {
                     Repeater {
                         id: commonParticipants
 
-                        model: GenericParticipantsFilterModel
+                        model: genericParticipantsModel
                         delegate: Loader {
                             sourceComponent: callVideoMedia
                             active: root.visible
diff --git a/src/app/mainview/components/ParticipantsLayoutVertical.qml b/src/app/mainview/components/ParticipantsLayoutVertical.qml
index 9c1526068107ae410dac4f45d96670eb056b739a..23a834e17ff90a9384dd8ba25a2b562273aa9ccf 100644
--- a/src/app/mainview/components/ParticipantsLayoutVertical.qml
+++ b/src/app/mainview/components/ParticipantsLayoutVertical.qml
@@ -19,8 +19,8 @@
 
 import QtQuick
 
-import QtQuick.Layouts 1.15
-import QtQuick.Controls 2.15
+import QtQuick.Layouts
+import QtQuick.Controls
 
 import net.jami.Adapters 1.1
 import net.jami.Models 1.1
@@ -169,7 +169,7 @@ SplitView {
                     Repeater {
                         id: commonParticipants
 
-                        model: GenericParticipantsFilterModel
+                        model: genericParticipantsModel
                         delegate: Loader {
                             sourceComponent: callVideoMedia
                             active: root.visible
@@ -265,7 +265,7 @@ SplitView {
             anchors.fill: parent
             anchors.centerIn: parent
 
-            model: ActiveParticipantsFilterModel
+            model: activeParticipantsModel
             delegate: Loader {
                 active: root.visible
                 asynchronous: true