From 0996b167d9efee6b3fa202a948a9984a299aa15f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Thu, 9 Jun 2022 11:23:14 -0400
Subject: [PATCH] swarm: add call buttons and interactions for multi-swarm

+ Add call buttons to start a new call
+ React to events from the swarm
+ call interactions (Join call/Call ended, etc)
+ active calls area
+ Add call management logic in LRC
+ Feature is enabled via the experimental checkbox

https://git.jami.net/savoirfairelinux/jami-daemon/-/issues/312
Change-Id: I83fd20b5e772097c0792bdc66feec69b0cb0009a
---
 daemon                                        |   2 +-
 src/app/appsettingsmanager.h                  |   1 +
 src/app/calladapter.cpp                       |  12 +-
 .../commoncomponents/CallMessageDelegate.qml  | 118 ++++++++++
 src/app/commoncomponents/SBSMessageBase.qml   |   1 +
 .../commoncomponents/TextMessageDelegate.qml  |   1 -
 src/app/constant/JamiStrings.qml              |  14 ++
 src/app/conversationlistmodelbase.cpp         |   3 +
 src/app/conversationlistmodelbase.h           |   1 +
 src/app/conversationsadapter.cpp              |  12 +-
 src/app/conversationsadapter.h                |   4 +
 src/app/currentaccount.cpp                    |   1 +
 src/app/currentaccount.h                      |   1 +
 src/app/currentconversation.cpp               | 113 ++++++++-
 src/app/currentconversation.h                 |  10 +
 src/app/lrcinstance.cpp                       |  13 +-
 src/app/lrcinstance.h                         |   3 +
 src/app/mainview/components/ChatView.qml      |  39 +++-
 .../mainview/components/ChatViewHeader.qml    |   9 +-
 .../components/ConversationErrorsRow.qml      |   1 +
 .../mainview/components/DevicesListPopup.qml  | 219 ++++++++++++++++++
 src/app/mainview/components/HostPopup.qml     | 111 +++++++++
 .../mainview/components/MessageListView.qml   |   2 +-
 .../mainview/components/NotificationArea.qml  | 117 ++++++++++
 .../components/SmartListItemDelegate.qml      |  17 +-
 .../mainview/components/SwarmDetailsPanel.qml | 113 +++++++++
 src/app/messagesadapter.cpp                   |  13 ++
 src/app/messagesadapter.h                     |   4 +
 .../components/TroubleshootSettings.qml       |  13 ++
 src/app/utilsadapter.cpp                      |   2 +
 src/app/utilsadapter.h                        |   1 +
 src/libclient/api/conversation.h              |  15 ++
 src/libclient/api/conversationmodel.h         |  35 ++-
 src/libclient/api/interaction.h               |   3 +
 src/libclient/authority/storagehelper.cpp     |  15 +-
 src/libclient/authority/storagehelper.h       |   6 +-
 src/libclient/callbackshandler.cpp            |  26 +++
 src/libclient/callbackshandler.h              |  19 ++
 src/libclient/callmodel.cpp                   |  55 ++++-
 src/libclient/conversationmodel.cpp           | 161 ++++++++++---
 src/libclient/messagelistmodel.cpp            |  25 ++
 src/libclient/messagelistmodel.h              |   4 +
 .../qtwrapper/configurationmanager_wrap.h     |  31 ++-
 43 files changed, 1276 insertions(+), 90 deletions(-)
 create mode 100644 src/app/commoncomponents/CallMessageDelegate.qml
 create mode 100644 src/app/mainview/components/DevicesListPopup.qml
 create mode 100644 src/app/mainview/components/HostPopup.qml
 create mode 100644 src/app/mainview/components/NotificationArea.qml

diff --git a/daemon b/daemon
index 54ffd0f43..08ef8dd80 160000
--- a/daemon
+++ b/daemon
@@ -1 +1 @@
-Subproject commit 54ffd0f4380bdbdc6a2fcb80847b2f5aefad4958
+Subproject commit 08ef8dd80d571816259b195b0956a472a40b58ad
diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h
index 6acca9096..272d972ca 100644
--- a/src/app/appsettingsmanager.h
+++ b/src/app/appsettingsmanager.h
@@ -54,6 +54,7 @@ extern const QString defaultDownloadPath;
     X(NeverShowMeAgain, false) \
     X(WindowGeometry, QRectF(qQNaN(), qQNaN(), 0., 0.)) \
     X(WindowState, QWindow::AutomaticVisibility) \
+    X(EnableExperimentalSwarm, false) \
     X(LANG, "SYSTEM")
 
 /*
diff --git a/src/app/calladapter.cpp b/src/app/calladapter.cpp
index 25c473c58..be21ec2d8 100644
--- a/src/app/calladapter.cpp
+++ b/src/app/calladapter.cpp
@@ -216,7 +216,8 @@ CallAdapter::onParticipantUpdated(const QString& callId, int index)
             return;
         }
         auto infos = getConferencesInfos();
-        participantsModel_->updateParticipant(index, infos[index]);
+        if (index < infos.size())
+            participantsModel_->updateParticipant(index, infos[index]);
     } catch (...) {
     }
 }
@@ -256,7 +257,8 @@ CallAdapter::onCallStatusChanged(const QString& callId, int code)
             const auto& currentConvInfo = lrcInstance_->getConversationFromConvUid(currentConvId);
 
             // was it a conference and now is a dialog?
-            if (currentConvInfo.confId.isEmpty() && currentConfSubcalls_.size() == 2) {
+            if (currentConvInfo.isCoreDialog() && currentConvInfo.confId.isEmpty()
+                && currentConfSubcalls_.size() == 2) {
                 auto it = std::find_if(currentConfSubcalls_.cbegin(),
                                        currentConfSubcalls_.cend(),
                                        [&callId](const QString& cid) { return cid != callId; });
@@ -495,14 +497,12 @@ CallAdapter::updateCall(const QString& convUid, const QString& accountId, bool f
     accountId_ = accountId.isEmpty() ? accountId_ : accountId;
 
     const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid);
-    if (convInfo.uid.isEmpty()) {
+    if (convInfo.uid.isEmpty())
         return;
-    }
 
     auto call = lrcInstance_->getCallInfoForConversation(convInfo, forceCallOnly);
-    if (!call) {
+    if (!call)
         return;
-    }
 
     if (convInfo.uid == lrcInstance_->get_selectedConvUid()) {
         auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_);
diff --git a/src/app/commoncomponents/CallMessageDelegate.qml b/src/app/commoncomponents/CallMessageDelegate.qml
new file mode 100644
index 000000000..52ece160f
--- /dev/null
+++ b/src/app/commoncomponents/CallMessageDelegate.qml
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import net.jami.Models 1.1
+import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+
+SBSMessageBase {
+    id: root
+
+    component JoinCallButton: PushButton {
+        visible: root.isActive
+        toolTipText: JamiStrings.joinCall
+        preferredSize: 40
+        imageColor: callLabel.color
+        normalColor: "transparent"
+        hoveredColor: Qt.rgba(255, 255, 255, 0.2)
+        border.width: 1
+        border.color: callLabel.color
+    }
+
+    property bool isRemoteImage
+
+    isOutgoing: Author === ""
+    author: Author
+    readers: Readers
+    formattedTime: MessagesAdapter.getFormattedTime(Timestamp)
+
+
+    Connections {
+        target: CurrentConversation
+        enabled: root.isActive
+
+        function onActiveCallsChanged() {
+            root.isActive = LRCInstance.indexOfActiveCall(ConfId, ActionUri, DeviceId) !== -1
+        }
+    }
+
+    property bool isActive: LRCInstance.indexOfActiveCall(ConfId, ActionUri, DeviceId) !== -1
+    visible: isActive || ConfId === "" || Duration > 0
+
+    bubble.color: {
+        if (ConfId === "" && Duration === 0) {
+            // If missed, we can add a darker pattern
+            return isOutgoing ?
+                        Qt.darker(JamiTheme.messageOutBgColor, 1.5) :
+                        Qt.darker(JamiTheme.messageInBgColor, 1.5)
+        }
+        return isOutgoing ?
+                    JamiTheme.messageOutBgColor :
+                    CurrentConversation.isCoreDialog ? JamiTheme.messageInBgColor : Qt.lighter(CurrentConversation.color, 1.5)
+    }
+
+    innerContent.children: [
+        RowLayout {
+            id: msg
+            anchors.right: isOutgoing ? parent.right : undefined
+            spacing: 10
+            visible: root.visible
+
+            Label {
+                id: callLabel
+                padding: 10
+                Layout.margins: 8
+                Layout.fillWidth: true
+
+                text:{
+                    if (root.isActive)
+                        return JamiStrings.joinCall
+                    return Body
+                }
+                horizontalAlignment: Qt.AlignHCenter
+                font.pointSize: JamiTheme.contactEventPointSize
+                font.bold: true
+                color: UtilsAdapter.luma(bubble.color) ?
+                       JamiTheme.chatviewTextColorLight :
+                       JamiTheme.chatviewTextColorDark
+            }
+
+            JoinCallButton {
+                id: joinCallInAudio
+
+                source: JamiResources.place_audiocall_24dp_svg
+                onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, ConfId, true)
+            }
+
+            JoinCallButton {
+                id: joinCallInVideo
+
+                source: JamiResources.videocam_24dp_svg
+                onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, ConfId)
+                Layout.rightMargin: parent.spacing
+            }
+        }
+    ]
+
+    opacity: 0
+    Behavior on opacity { NumberAnimation { duration: 100 } }
+    Component.onCompleted: opacity = 1
+}
\ No newline at end of file
diff --git a/src/app/commoncomponents/SBSMessageBase.qml b/src/app/commoncomponents/SBSMessageBase.qml
index 13ccfd497..50847d957 100644
--- a/src/app/commoncomponents/SBSMessageBase.qml
+++ b/src/app/commoncomponents/SBSMessageBase.qml
@@ -56,6 +56,7 @@ Control {
     readonly property real hPadding: JamiTheme.sbsMessageBasePreferredPadding
     width: ListView.view ? ListView.view.width : 0
     height: mainColumnLayout.implicitHeight
+
     rightPadding: hPadding
     leftPadding: hPadding
 
diff --git a/src/app/commoncomponents/TextMessageDelegate.qml b/src/app/commoncomponents/TextMessageDelegate.qml
index 93fcc59ad..7b1a9118c 100644
--- a/src/app/commoncomponents/TextMessageDelegate.qml
+++ b/src/app/commoncomponents/TextMessageDelegate.qml
@@ -70,7 +70,6 @@ SBSMessageBase {
                     Math.min(implicitWidth, innerContent.width - senderMargin)
             }
 
-            height: implicitHeight
             wrapMode: Label.WrapAtWordBoundaryOrAnywhere
             selectByMouse: true
             font.pixelSize: isEmojiOnly? JamiTheme.chatviewEmojiSize : JamiTheme.chatviewFontSize
diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml
index 070de9197..6a469b55b 100644
--- a/src/app/constant/JamiStrings.qml
+++ b/src/app/constant/JamiStrings.qml
@@ -487,6 +487,9 @@ Item {
     property string troubleshootButton: qsTr("Open logs")
     property string troubleshootText: qsTr("Get logs")
 
+    property string experimentalCallSwarm: qsTr("(Experimental) Enable call support for swarm")
+    property string experimentalCallSwarmTooltip: qsTr("This feature will enable call buttons in swarms with multiple participants.")
+
     // Recording Settings
     property string tipRecordFolder: qsTr("Select a record directory")
     property string quality: qsTr("Quality")
@@ -727,6 +730,15 @@ Item {
     property string writeTo: qsTr("Write to %1")
     property string edit: qsTr("Edit")
     property string edited: qsTr("Edited")
+    property string joinCall: qsTr("Join call")
+    property string wantToJoin: qsTr("A call is in progress. Do you want to join the call?")
+    property string needsHost: qsTr("Current host for this swarm seems unreachable. Do you want to host the call?")
+    property string chooseHoster: qsTr("Choose a dedicated device for hosting future calls in this swarm. If not set, the device starting a call will host it.")
+    property string chooseThisDevice: qsTr("Choose this device")
+    property string removeCurrentDevice: qsTr("Remove current device")
+    property string becomeHostOneCall: qsTr("Host only this call")
+    property string hostThisCall: qsTr("Host this call")
+    property string becomeDefaultHost: qsTr("Make me the default host for future calls")
 
     // Invitation View
     property string invitationViewSentRequest: qsTr("%1 has sent you a request for a conversation.")
@@ -745,9 +757,11 @@ Item {
     property string muteConversation: qsTr("Mute conversation")
     property string ignoreNotificationsTooltip: qsTr("Ignore all notifications from this conversation")
     property string chooseAColor: qsTr("Choose a color")
+    property string defaultCallHost: qsTr("Default host (calls)")
     property string leaveTheSwarm: qsTr("Leave the swarm")
     property string leave: qsTr("Leave")
     property string typeOfSwarm: qsTr("Type of swarm")
+    property string none: qsTr("None")
 
     // NewSwarmPage
     property string youCanAdd8: qsTr("You can add 8 people in the swarm")
diff --git a/src/app/conversationlistmodelbase.cpp b/src/app/conversationlistmodelbase.cpp
index 827c84bd9..b722e1fd4 100644
--- a/src/app/conversationlistmodelbase.cpp
+++ b/src/app/conversationlistmodelbase.cpp
@@ -94,6 +94,9 @@ ConversationListModelBase::dataForItem(item_t item, int role) const
             return lrcInstance_->getContentDraft(item.uid, item.accountId);
         return {};
     }
+    case Role::ActiveCallsCount: {
+        return item.activeCalls.size();
+    }
     case Role::IsRequest:
         return QVariant(item.isRequest);
     case Role::Title:
diff --git a/src/app/conversationlistmodelbase.h b/src/app/conversationlistmodelbase.h
index f22e1c923..a47b3dc05 100644
--- a/src/app/conversationlistmodelbase.h
+++ b/src/app/conversationlistmodelbase.h
@@ -44,6 +44,7 @@
     X(CallState) \
     X(SectionName) \
     X(AccountId) \
+    X(ActiveCallsCount) \
     X(Draft) \
     X(IsRequest) \
     X(Mode) \
diff --git a/src/app/conversationsadapter.cpp b/src/app/conversationsadapter.cpp
index 6f973726f..9b47615ba 100644
--- a/src/app/conversationsadapter.cpp
+++ b/src/app/conversationsadapter.cpp
@@ -59,7 +59,7 @@ ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
         } else {
             // selected
             const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
-            if (convInfo.uid.isEmpty())
+            if (convInfo.uid.isEmpty() || convInfo.accountId != lrcInstance_->get_currentAccountId())
                 return;
 
             auto& accInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
@@ -528,6 +528,16 @@ ConversationsAdapter::popFrontError(const QString& convId)
     convModel->popFrontError(convId);
 }
 
+void
+ConversationsAdapter::ignoreActiveCall(const QString& convId,
+                                       const QString& id,
+                                       const QString& uri,
+                                       const QString& device)
+{
+    auto convModel = lrcInstance_->getCurrentConversationModel();
+    convModel->ignoreActiveCall(convId, id, uri, device);
+}
+
 void
 ConversationsAdapter::updateConversationDescription(const QString& convId,
                                                     const QString& newDescription)
diff --git a/src/app/conversationsadapter.h b/src/app/conversationsadapter.h
index 0685f1032..bf908cf39 100644
--- a/src/app/conversationsadapter.h
+++ b/src/app/conversationsadapter.h
@@ -59,6 +59,10 @@ public:
     Q_INVOKABLE void restartConversation(const QString& convId);
     Q_INVOKABLE void updateConversationTitle(const QString& convId, const QString& newTitle);
     Q_INVOKABLE void popFrontError(const QString& convId);
+    Q_INVOKABLE void ignoreActiveCall(const QString& convId,
+                                      const QString& id,
+                                      const QString& uri,
+                                      const QString& device);
     Q_INVOKABLE void updateConversationDescription(const QString& convId,
                                                    const QString& newDescription);
 
diff --git a/src/app/currentaccount.cpp b/src/app/currentaccount.cpp
index 440744ef2..319678dcf 100644
--- a/src/app/currentaccount.cpp
+++ b/src/app/currentaccount.cpp
@@ -111,6 +111,7 @@ CurrentAccount::updateData()
         set_enabled(accInfo.enabled);
         set_managerUri(accConfig.managerUri);
         set_keepAliveEnabled(accConfig.keepAliveEnabled, true);
+        set_deviceId(accConfig.deviceId);
         set_peerDiscovery(accConfig.peerDiscovery, true);
         set_sendReadReceipt(accConfig.sendReadReceipt, true);
         set_isRendezVous(accConfig.isRendezVous, true);
diff --git a/src/app/currentaccount.h b/src/app/currentaccount.h
index 4f99b1045..d359658fe 100644
--- a/src/app/currentaccount.h
+++ b/src/app/currentaccount.h
@@ -101,6 +101,7 @@ class CurrentAccount final : public QObject
 
     QML_RO_PROPERTY(QString, id)
     QML_RO_PROPERTY(QString, uri)
+    QML_RO_PROPERTY(QString, deviceId)
     QML_RO_PROPERTY(QString, registeredName)
     QML_RO_PROPERTY(QString, alias)
     QML_RO_PROPERTY(QString, bestId)
diff --git a/src/app/currentconversation.cpp b/src/app/currentconversation.cpp
index e2cbc264d..787fa2429 100644
--- a/src/app/currentconversation.cpp
+++ b/src/app/currentconversation.cpp
@@ -53,8 +53,6 @@ CurrentConversation::updateData()
         const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
         if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) {
             auto& convInfo = optConv->get();
-            set_title(accInfo.conversationModel->title(convId));
-            set_description(accInfo.conversationModel->description(convId));
             set_uris(convInfo.participantsUris());
             set_isSwarm(convInfo.isSwarm());
             set_isLegacy(convInfo.isLegacy());
@@ -104,6 +102,9 @@ CurrentConversation::updateData()
             } else if (convInfo.mode == conversation::Mode::PUBLIC) {
                 set_modeString(tr("Public group"));
             }
+
+            onProfileUpdated(convId);
+            updateActiveCalls(accountId, convId);
         }
     } catch (...) {
         qWarning() << "Can't update current conversation data for" << convId;
@@ -111,33 +112,58 @@ CurrentConversation::updateData()
     updateErrors(convId);
 }
 
+void
+CurrentConversation::onNeedsHost(const QString& convId)
+{
+    if (id_ != convId)
+        return;
+    Q_EMIT needsHost();
+}
+
 void
 CurrentConversation::setPreference(const QString& key, const QString& value)
 {
+    if (key == "color")
+        set_color(value);
+    auto preferences = getPreferences();
+    preferences[key] = value;
     auto accountId = lrcInstance_->get_currentAccountId();
     const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
     auto convId = lrcInstance_->get_selectedConvUid();
-    if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) {
-        auto& convInfo = optConv->get();
-        auto preferences = convInfo.preferences;
-        preferences[key] = value;
-        accInfo.conversationModel->setConversationPreferences(convId, preferences);
-    }
+    accInfo.conversationModel->setConversationPreferences(convId, preferences);
 }
 
 QString
 CurrentConversation::getPreference(const QString& key) const
+{
+    return getPreferences()[key];
+}
+
+MapStringString
+CurrentConversation::getPreferences() const
 {
     auto accountId = lrcInstance_->get_currentAccountId();
     const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
     auto convId = lrcInstance_->get_selectedConvUid();
     if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) {
         auto& convInfo = optConv->get();
-        return convInfo.preferences[key];
+        auto preferences = accInfo.conversationModel->getConversationPreferences(convId);
+        return preferences;
     }
     return {};
 }
 
+void
+CurrentConversation::setInfo(const QString& key, const QString& value)
+{
+    MapStringString infos;
+    infos[key] = value;
+    auto accountId = lrcInstance_->get_currentAccountId();
+    const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
+    auto convId = lrcInstance_->get_selectedConvUid();
+    accInfo.conversationModel->updateConversationInfos(convId, infos);
+}
+
 void
 CurrentConversation::onConversationUpdated(const QString& convId)
 {
@@ -153,8 +179,27 @@ CurrentConversation::onProfileUpdated(const QString& convId)
     // filter for our currently set id
     if (id_ != convId)
         return;
-    set_title(lrcInstance_->getCurrentConversationModel()->title(convId));
-    set_description(lrcInstance_->getCurrentConversationModel()->description(convId));
+    const auto& convModel = lrcInstance_->getCurrentConversationModel();
+    set_title(convModel->title(convId));
+    set_description(convModel->description(convId));
+
+    try {
+        if (auto optConv = convModel->getConversationForUid(convId)) {
+            auto& convInfo = optConv->get();
+            // Now, update call informations (rdvAccount/device)
+            if (convInfo.infos.contains("rdvAccount")) {
+                set_rdvAccount(convInfo.infos["rdvAccount"]);
+            } else {
+                set_rdvAccount("");
+            }
+            if (convInfo.infos.contains("rdvDevice")) {
+                set_rdvDevice(convInfo.infos["rdvDevice"]);
+            } else {
+                set_rdvDevice("");
+            }
+        }
+    } catch (...) {
+    }
 }
 
 void
@@ -200,11 +245,21 @@ CurrentConversation::connectModel()
             this,
             &CurrentConversation::updateErrors,
             Qt::UniqueConnection);
+    connect(lrcInstance_->getCurrentConversationModel(),
+            &ConversationModel::activeCallsChanged,
+            this,
+            &CurrentConversation::updateActiveCalls,
+            Qt::UniqueConnection);
     connect(lrcInstance_->getCurrentConversationModel(),
             &ConversationModel::conversationPreferencesUpdated,
             this,
             &CurrentConversation::updateConversationPreferences,
             Qt::UniqueConnection);
+    connect(lrcInstance_->getCurrentConversationModel(),
+            &ConversationModel::needsHost,
+            this,
+            &CurrentConversation::onNeedsHost,
+            Qt::UniqueConnection);
 }
 
 void
@@ -246,6 +301,42 @@ CurrentConversation::updateErrors(const QString& convId)
     }
 }
 
+void
+CurrentConversation::updateActiveCalls(const QString&, const QString& convId)
+{
+    if (convId != id_)
+        return;
+    const auto& convModel = lrcInstance_->getCurrentConversationModel();
+    if (auto optConv = convModel->getConversationForUid(convId)) {
+        auto& convInfo = optConv->get();
+        QVariantList callList;
+        for (int i = 0; i < convInfo.activeCalls.size(); i++) {
+            // Check if ignored.
+            auto ignored = false;
+            for (int ignoredIdx = 0; ignoredIdx < convInfo.ignoredActiveCalls.size(); ignoredIdx++) {
+                auto& ignoreCall = convInfo.ignoredActiveCalls[ignoredIdx];
+                if (ignoreCall["id"] == convInfo.activeCalls[i]["id"]
+                    && ignoreCall["uri"] == convInfo.activeCalls[i]["uri"]
+                    && ignoreCall["device"] == convInfo.activeCalls[i]["device"]) {
+                    ignored = true;
+                    break;
+                }
+            }
+            if (ignored) {
+                continue;
+            }
+
+            // Else, add to model
+            QVariantMap mapCall;
+            Q_FOREACH (QString key, convInfo.activeCalls[i].keys()) {
+                mapCall[key] = convInfo.activeCalls[i][key];
+            }
+            callList.append(mapCall);
+        }
+        set_activeCalls(callList);
+    }
+}
+
 void
 CurrentConversation::scrollToMsg(const QString& msg)
 {
diff --git a/src/app/currentconversation.h b/src/app/currentconversation.h
index 25f705305..62c7944f2 100644
--- a/src/app/currentconversation.h
+++ b/src/app/currentconversation.h
@@ -43,12 +43,15 @@ class CurrentConversation final : public QObject
     QML_PROPERTY(bool, ignoreNotifications)
     QML_PROPERTY(QString, callId)
     QML_PROPERTY(QString, color)
+    QML_PROPERTY(QString, rdvAccount)
+    QML_PROPERTY(QString, rdvDevice)
     QML_PROPERTY(call::Status, callState)
     QML_PROPERTY(bool, inCall)
     QML_PROPERTY(bool, isTemporary)
     QML_PROPERTY(bool, isContact)
     QML_PROPERTY(bool, allMessagesLoaded)
     QML_PROPERTY(QString, modeString)
+    QML_PROPERTY(QVariantList, activeCalls)
     QML_PROPERTY(QStringList, errors)
     QML_PROPERTY(QStringList, backendErrors)
 
@@ -63,6 +66,8 @@ public:
     Q_INVOKABLE void showSwarmDetails() const;
     Q_INVOKABLE void setPreference(const QString& key, const QString& value);
     Q_INVOKABLE QString getPreference(const QString& key) const;
+    Q_INVOKABLE MapStringString getPreferences() const;
+    Q_INVOKABLE void setInfo(const QString& key, const QString& value);
 
 Q_SIGNALS:
     void scrollTo(const QString& msgId);
@@ -70,10 +75,15 @@ Q_SIGNALS:
 
 private Q_SLOTS:
     void updateData();
+    void onNeedsHost(const QString& convId);
     void onConversationUpdated(const QString& convId);
     void onProfileUpdated(const QString& convId);
     void updateErrors(const QString& convId);
     void updateConversationPreferences(const QString& convId);
+    void updateActiveCalls(const QString&, const QString& convId);
+
+Q_SIGNALS:
+    void needsHost();
 
 private:
     LRCInstance* lrcInstance_;
diff --git a/src/app/lrcinstance.cpp b/src/app/lrcinstance.cpp
index 21b2254cf..c5d3ee731 100644
--- a/src/app/lrcinstance.cpp
+++ b/src/app/lrcinstance.cpp
@@ -190,9 +190,8 @@ LRCInstance::getCallInfoForConversation(const conversation::Info& convInfo, bool
         auto callId = forceCallOnly
                           ? convInfo.callId
                           : (convInfo.confId.isEmpty() ? convInfo.callId : convInfo.confId);
-        if (!accInfo.callModel->hasCall(callId)) {
+        if (!accInfo.callModel->hasCall(callId))
             return nullptr;
-        }
         return &accInfo.callModel->getCall(callId);
     } catch (...) {
         return nullptr;
@@ -372,6 +371,16 @@ LRCInstance::selectConversation(const QString& convId, const QString& accountId)
     set_selectedConvUid(convId);
 }
 
+int
+LRCInstance::indexOfActiveCall(const QString& confId, const QString& uri, const QString& deviceId)
+{
+    if (auto optConv = getCurrentConversationModel()->getConversationForUid(selectedConvUid_)) {
+        auto& convInfo = optConv->get();
+        return convInfo.indexOfActiveCall({{"confId", confId}, {"uri", uri}, {"device", deviceId}});
+    }
+    return -1;
+}
+
 void
 LRCInstance::deselectConversation()
 {
diff --git a/src/app/lrcinstance.h b/src/app/lrcinstance.h
index d8e7a495f..ae8633ace 100644
--- a/src/app/lrcinstance.h
+++ b/src/app/lrcinstance.h
@@ -108,6 +108,9 @@ public:
     Q_INVOKABLE void setContentDraft(const QString& convUid,
                                      const QString& accountId,
                                      const QString& content);
+    Q_INVOKABLE int indexOfActiveCall(const QString& confId,
+                                      const QString& uri,
+                                      const QString& deviceId);
 
     int getCurrentAccountIndex();
     void setCurrAccDisplayName(const QString& displayName);
diff --git a/src/app/mainview/components/ChatView.qml b/src/app/mainview/components/ChatView.qml
index ee90d9251..d8d946fe9 100644
--- a/src/app/mainview/components/ChatView.qml
+++ b/src/app/mainview/components/ChatView.qml
@@ -47,6 +47,10 @@ Rectangle {
 
     color: JamiTheme.chatviewBgColor
 
+    HostPopup {
+        id: hostPopup
+    }
+
     ColumnLayout {
         anchors.fill: root
 
@@ -88,6 +92,10 @@ Rectangle {
                         addMemberPanel.visible = !addMemberPanel.visible
                     }
                 }
+
+                function onNeedsHost() {
+                    hostPopup.open()
+                }
             }
 
             onAddToConversationClicked: {
@@ -105,9 +113,38 @@ Rectangle {
             }
         }
 
+
+        Connections {
+            target: CurrentConversation
+            enabled: true
+
+            function onActiveCallsChanged() {
+                if (CurrentConversation.activeCalls.length > 0) {
+                    notificationArea.id = CurrentConversation.activeCalls[0]["id"]
+                    notificationArea.uri = CurrentConversation.activeCalls[0]["uri"]
+                    notificationArea.device = CurrentConversation.activeCalls[0]["device"]
+                }
+                notificationArea.visible = CurrentConversation.activeCalls.length > 0
+            }
+
+            function onErrorsChanged() {
+                if (CurrentConversation.errors.length > 0) {
+                    errorRect.errorLabel.text = CurrentConversation.errors[0]
+                    errorRect.backendErrorToolTip.text = JamiStrings.backendError.arg(CurrentConversation.backendErrors[0])
+                }
+                errorRect.visible = CurrentConversation.errors.length > 0 // If too much noise: && LRCInstance.debugMode()
+            }
+        }
+
         ConversationErrorsRow {
             id: errorRect
-            color: JamiTheme.filterBadgeColor
+            Layout.fillWidth: true
+            Layout.preferredHeight: JamiTheme.chatViewHeaderPreferredHeight
+            visible: false
+        }
+
+        NotificationArea {
+            id: notificationArea
             Layout.fillWidth: true
             Layout.preferredHeight: JamiTheme.chatViewHeaderPreferredHeight
             visible: false
diff --git a/src/app/mainview/components/ChatViewHeader.qml b/src/app/mainview/components/ChatViewHeader.qml
index 60cb8a109..51e44d6eb 100644
--- a/src/app/mainview/components/ChatViewHeader.qml
+++ b/src/app/mainview/components/ChatViewHeader.qml
@@ -20,9 +20,10 @@
 import QtQuick
 import QtQuick.Layouts
 
-import net.jami.Models 1.1
-import net.jami.Constants 1.1
 import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+import net.jami.Enums 1.1
+import net.jami.Models 1.1
 
 import "../../commoncomponents"
 
@@ -155,7 +156,7 @@ Rectangle {
             PushButton {
                 id: startAAudioCallButton
 
-                visible: interactionButtonsVisibility && !addMemberVisibility
+                visible: interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm))
 
                 source: JamiResources.place_audiocall_24dp_svg
                 toolTipText: JamiStrings.placeAudioCall
@@ -169,7 +170,7 @@ Rectangle {
             PushButton {
                 id: startAVideoCallButton
 
-                visible: CurrentAccount.videoEnabled_Video && interactionButtonsVisibility && !addMemberVisibility
+                visible: CurrentAccount.videoEnabled_Video && interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm))
                 source: JamiResources.videocam_24dp_svg
                 toolTipText: JamiStrings.placeVideoCall
 
diff --git a/src/app/mainview/components/ConversationErrorsRow.qml b/src/app/mainview/components/ConversationErrorsRow.qml
index 9b08fad6a..8fdc2cc58 100644
--- a/src/app/mainview/components/ConversationErrorsRow.qml
+++ b/src/app/mainview/components/ConversationErrorsRow.qml
@@ -43,6 +43,7 @@ Rectangle {
             errorRect.visible = CurrentConversation.errors.length > 0 && LRCInstance.debugMode()
         }
     }
+    color: JamiTheme.filterBadgeColor
 
     RowLayout {
         anchors.fill: parent
diff --git a/src/app/mainview/components/DevicesListPopup.qml b/src/app/mainview/components/DevicesListPopup.qml
new file mode 100644
index 000000000..3e3f97e64
--- /dev/null
+++ b/src/app/mainview/components/DevicesListPopup.qml
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2020-2022 Savoir-faire Linux Inc.
+ *
+ * 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
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+
+import SortFilterProxyModel 0.2
+
+import net.jami.Models 1.1
+import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+
+import "../../commoncomponents"
+
+BaseModalDialog {
+    id: root
+
+    width: 488
+    height: 320
+
+    popupContent: Rectangle {
+        id: rect
+
+        color: JamiTheme.transparentColor
+        width: root.width
+
+
+        PushButton {
+            id: btnCancel
+            imageColor: "grey"
+            normalColor: "transparent"
+            anchors.right: parent.right
+            anchors.top: parent.top
+            anchors.topMargin: 10
+            anchors.rightMargin: 10
+            source: JamiResources.round_close_24dp_svg
+            onClicked: { close(); }
+        }
+
+        ColumnLayout {
+            id: mainLayout
+            anchors.fill: parent
+            anchors.margins: JamiTheme.preferredMarginSize
+            spacing: JamiTheme.preferredMarginSize
+
+            Label {
+                id: informativeLabel
+
+                Layout.alignment: Qt.AlignCenter
+                Layout.fillWidth: true
+                Layout.topMargin: 26
+                wrapMode: Text.WordWrap
+                horizontalAlignment: Text.AlignHCenter
+                verticalAlignment: Text.AlignVCenter
+                text: JamiStrings.chooseHoster
+                color: JamiTheme.primaryForegroundColor
+            }
+
+            JamiListView {
+                id: devicesListView
+
+                Layout.fillWidth: true
+                Layout.preferredHeight: 160
+
+                model: SortFilterProxyModel {
+                    sourceModel: DeviceItemListModel
+                    sorters: [
+                        RoleSorter { roleName: "IsCurrent"; sortOrder: Qt.DescendingOrder },
+                        StringSorter {
+                            roleName: "DeviceName"
+                            caseSensitivity: Qt.CaseInsensitive
+                        }
+                    ]
+                }
+
+                delegate: ItemDelegate {
+                    id: item
+
+                    property string deviceName : DeviceName
+                    property string deviceId : DeviceID
+                    property bool isCurrent : DeviceName
+
+                    implicitWidth: devicesListView.width
+                    width: devicesListView.width
+                    height: 70
+
+                    highlighted: ListView.isCurrentItem
+
+                    MouseArea {
+                        anchors.fill: parent
+                        onClicked: {
+                            devicesListView.currentIndex = index
+                        }
+                    }
+
+                    background: Rectangle {
+                        color: highlighted? JamiTheme.selectedColor : JamiTheme.editBackgroundColor
+                    }
+
+                    RowLayout {
+                        anchors.fill: item
+
+                        Image {
+                            id: deviceImage
+
+                            Layout.alignment: Qt.AlignVCenter
+                            Layout.preferredWidth: 24
+                            Layout.preferredHeight: 24
+                            Layout.leftMargin: JamiTheme.preferredMarginSize
+
+                            layer {
+                                enabled: true
+                                effect: ColorOverlay {
+                                    color: JamiTheme.textColor
+                                }
+                            }
+                            source: JamiResources.baseline_desktop_windows_24dp_svg
+                        }
+
+                        ColumnLayout {
+                            id: deviceInfoColumnLayout
+
+                            Layout.fillWidth: true
+                            Layout.fillHeight: true
+                            Layout.leftMargin: JamiTheme.preferredMarginSize
+
+                            Text {
+                                id: labelDeviceName
+
+                                Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+                                Layout.fillWidth: true
+
+                                elide: Text.ElideRight
+                                color: JamiTheme.textColor
+                                text: deviceName
+                            }
+
+                            Text {
+                                id: labelDeviceId
+
+                                Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+                                Layout.fillWidth: true
+
+                                elide: Text.ElideRight
+                                color: JamiTheme.textColor
+                                text: deviceId === "" ? qsTr("Device Id") : deviceId
+                            }
+                        }
+                    }
+
+                    CustomBorder {
+                        commonBorder: false
+                        lBorderwidth: 0
+                        rBorderwidth: 0
+                        tBorderwidth: 0
+                        bBorderwidth: 2
+                        borderColor: JamiTheme.selectedColor
+                    }
+                }
+            }
+
+            RowLayout {
+                spacing: JamiTheme.preferredMarginSize
+                Layout.preferredWidth: parent.width
+
+                MaterialButton {
+                    id: chooseBtn
+
+                    Layout.alignment: Qt.AlignCenter
+                    enabled: devicesListView.currentItem
+
+                    text: JamiStrings.chooseThisDevice
+                    toolTipText: JamiStrings.chooseThisDevice
+
+                    onClicked: {
+                        CurrentConversation.setInfo("rdvAccount", CurrentAccount.uri)
+                        CurrentConversation.setInfo("rdvDevice", devicesListView.currentItem.deviceId)
+                        close()
+                    }
+                }
+
+                MaterialButton {
+                    id: rmDeviceBtn
+
+                    Layout.alignment: Qt.AlignCenter
+                    enabled: devicesListView.currentItem
+
+                    text: JamiStrings.removeCurrentDevice
+                    toolTipText: JamiStrings.removeCurrentDevice
+
+                    onClicked: {
+                        CurrentConversation.setInfo("rdvAccount", "")
+                        CurrentConversation.setInfo("rdvDevice", "")
+                        close()
+                    }
+                }
+            }
+
+
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/app/mainview/components/HostPopup.qml b/src/app/mainview/components/HostPopup.qml
new file mode 100644
index 000000000..9f2a64bad
--- /dev/null
+++ b/src/app/mainview/components/HostPopup.qml
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * 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
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+
+import net.jami.Models 1.1
+import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+
+import "../../commoncomponents"
+
+BaseModalDialog {
+    id: root
+
+    width: 488
+    height: 256
+
+    property bool isAdmin: {
+        var role = UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, CurrentAccount.uri)
+        return role === Member.Role.ADMIN
+    }
+
+
+    popupContent: Rectangle {
+        id: rect
+
+        color: JamiTheme.transparentColor
+        width: root.width
+
+
+        PushButton {
+            id: btnCancel
+            imageColor: "grey"
+            normalColor: "transparent"
+            anchors.right: parent.right
+            anchors.top: parent.top
+            anchors.topMargin: 10
+            anchors.rightMargin: 10
+            source: JamiResources.round_close_24dp_svg
+            onClicked: { close();}
+        }
+
+        ColumnLayout {
+            id: mainLayout
+            anchors.fill: parent
+            anchors.margins: JamiTheme.preferredMarginSize
+
+            Label {
+                id: informativeLabel
+
+                Layout.alignment: Qt.AlignCenter
+                Layout.fillWidth: true
+                Layout.topMargin: 26
+                wrapMode: Text.WordWrap
+                horizontalAlignment: Text.AlignHCenter
+                verticalAlignment: Text.AlignVCenter
+                text: JamiStrings.needsHost
+                color: JamiTheme.primaryForegroundColor
+            }
+
+            MaterialButton {
+                id: becomeHostBtn
+
+                Layout.alignment: Qt.AlignCenter
+
+                Layout.topMargin: 26
+                text: isAdmin? JamiStrings.becomeHostOneCall : JamiStrings.hostThisCall
+
+                onClicked: {
+                    MessagesAdapter.joinCall(CurrentAccount.uri, CurrentAccount.deviceId, "0")
+                    close()
+                }
+            }
+
+            MaterialButton {
+                id: becomeDefaultHostBtn
+
+                Layout.alignment: Qt.AlignCenter
+
+                text: JamiStrings.becomeDefaultHost
+                toolTipText: JamiStrings.becomeDefaultHost
+
+                visible: isAdmin
+
+                onClicked: {
+                    CurrentConversation.setInfo("rdvAccount", CurrentAccount.uri)
+                    CurrentConversation.setInfo("rdvDevice", devicesListView.currentItem.deviceId)
+                    MessagesAdapter.joinCall(CurrentAccount.uri, CurrentAccount.deviceId, "0")
+                    close()
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/app/mainview/components/MessageListView.qml b/src/app/mainview/components/MessageListView.qml
index b30564884..bd2e971b9 100644
--- a/src/app/mainview/components/MessageListView.qml
+++ b/src/app/mainview/components/MessageListView.qml
@@ -241,7 +241,7 @@ JamiListView {
         DelegateChoice {
             roleValue: Interaction.Type.CALL
 
-            GeneratedMessageDelegate {
+            CallMessageDelegate {
                 Component.onCompleted:  {
                     computeChatview(this, index)
                 }
diff --git a/src/app/mainview/components/NotificationArea.qml b/src/app/mainview/components/NotificationArea.qml
new file mode 100644
index 000000000..be025ba00
--- /dev/null
+++ b/src/app/mainview/components/NotificationArea.qml
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * 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
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+
+import net.jami.Models 1.1
+import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+
+import "../../commoncomponents"
+
+Rectangle {
+    id: root
+
+    opacity: visible
+    color: CurrentConversation.color
+
+    property string id: ""
+    property string uri: ""
+    property string device: ""
+
+    property string textColor: UtilsAdapter.luma(root.color) ?
+                            JamiTheme.chatviewTextColorLight :
+                            JamiTheme.chatviewTextColorDark
+    RowLayout {
+        anchors.fill: parent
+        anchors.margins: JamiTheme.preferredMarginSize
+        spacing: 0
+
+        Text {
+            id: errorLabel
+            Layout.alignment: Qt.AlignVCenter
+            Layout.margins: 0
+            text: JamiStrings.wantToJoin
+            color: root.textColor
+            font.pixelSize: JamiTheme.headerFontSize
+            elide: Text.ElideRight
+        }
+
+        RowLayout {
+            id: controls
+            Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+
+            PushButton {
+                id: joinCallInAudio
+                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+                Layout.rightMargin: JamiTheme.preferredMarginSize
+
+                source: JamiResources.place_audiocall_24dp_svg
+                toolTipText: JamiStrings.joinCall
+
+                imageColor: root.textColor
+                normalColor: "transparent"
+                hoveredColor: Qt.rgba(255, 255, 255, 0.2)
+                border.width: 1
+                border.color: root.textColor
+
+                onClicked: MessagesAdapter.joinCall(uri, device, id, true)
+            }
+
+
+            PushButton {
+                id: joinCallInVideo
+                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+                Layout.rightMargin: JamiTheme.preferredMarginSize
+
+                source: JamiResources.videocam_24dp_svg
+                toolTipText: JamiStrings.joinCall
+
+                imageColor: root.textColor
+                normalColor: "transparent"
+                hoveredColor: Qt.rgba(255, 255, 255, 0.2)
+                border.width: 1
+                border.color: root.textColor
+                visible: CurrentAccount.videoEnabled_Video
+
+                onClicked: MessagesAdapter.joinCall(uri, device, id)
+            }
+
+            PushButton {
+                id: btnClose
+                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+
+                imageColor: root.textColor
+                normalColor: JamiTheme.transparentColor
+
+                source: JamiResources.round_close_24dp_svg
+
+                onClicked: ConversationsAdapter.ignoreActiveCall(CurrentConversation.id, id, uri, device)
+            }
+        }
+    }
+
+    Behavior on opacity {
+        NumberAnimation {
+            from: 0
+            duration: JamiTheme.shortFadeDuration
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/app/mainview/components/SmartListItemDelegate.qml b/src/app/mainview/components/SmartListItemDelegate.qml
index 563610587..6b6f9e32c 100644
--- a/src/app/mainview/components/SmartListItemDelegate.qml
+++ b/src/app/mainview/components/SmartListItemDelegate.qml
@@ -22,9 +22,10 @@ import QtQuick.Controls
 import QtQuick.Layouts
 import Qt5Compat.GraphicalEffects
 
-import net.jami.Models 1.1
 import net.jami.Adapters 1.1
 import net.jami.Constants 1.1
+import net.jami.Enums 1.1
+import net.jami.Models 1.1
 
 import "../../commoncomponents"
 
@@ -148,11 +149,6 @@ ItemDelegate {
                     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
                 }
             }
             Text {
@@ -175,6 +171,13 @@ ItemDelegate {
             color: JamiTheme.primaryForegroundColor
         }
 
+        // Show that a call is ongoing for groups indicator
+        ResponsiveImage {
+            visible: ActiveCallsCount && !root.highlighted
+            source: JamiResources.videocam_24dp_svg
+            color: JamiTheme.primaryForegroundColor
+        }
+
         ColumnLayout {
             Layout.fillHeight: true
             spacing: 2
@@ -232,6 +235,8 @@ ItemDelegate {
         if (!interactive)
             return;
         ListView.view.model.select(index)
+        if (CurrentConversation.isSwarm && !CurrentConversation.isCoreDialog && !UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm))
+            return; // For now disable calls for swarm with multiple participants
         if (LRCInstance.currentAccountType === Profile.Type.SIP || !CurrentAccount.videoEnabled_Video)
             CallAdapter.placeAudioOnlyCall()
         else {
diff --git a/src/app/mainview/components/SwarmDetailsPanel.qml b/src/app/mainview/components/SwarmDetailsPanel.qml
index 7625a76e9..aa1af54e7 100644
--- a/src/app/mainview/components/SwarmDetailsPanel.qml
+++ b/src/app/mainview/components/SwarmDetailsPanel.qml
@@ -35,6 +35,11 @@ Rectangle {
     color: CurrentConversation.color
     property var isAdmin: UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, CurrentAccount.uri) === Member.Role.ADMIN
 
+
+    DevicesListPopup {
+        id: devicesListPopup
+    }
+
     ColumnLayout {
         id: swarmProfileDetails
         Layout.fillHeight: true
@@ -343,6 +348,114 @@ Rectangle {
                     }
                 }
 
+                SwarmDetailsItem {
+                    id: settingsSwarmItem
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: JamiTheme.settingsFontSize + 2 * JamiTheme.preferredMarginSize + 4
+
+                    RowLayout {
+                        anchors.fill: parent
+                        anchors.leftMargin: JamiTheme.preferredMarginSize
+
+                        Text {
+                            id: settingsSwarmText
+                            Layout.fillWidth: true
+                            Layout.preferredHeight: 30
+                            Layout.rightMargin: JamiTheme.preferredMarginSize
+                            Layout.maximumWidth: settingsSwarmItem.width / 2
+
+                            text: JamiStrings.defaultCallHost
+                            font.pointSize: JamiTheme.settingsFontSize
+                            font.kerning: true
+                            elide: Text.ElideRight
+                            horizontalAlignment: Text.AlignLeft
+                            verticalAlignment: Text.AlignVCenter
+
+                            color: JamiTheme.textColor
+                        }
+
+
+                        RowLayout {
+                            id: swarmRdvPref
+                            spacing: 10
+                            Layout.alignment: Qt.AlignRight
+                            Layout.maximumWidth: settingsSwarmItem.width / 2
+
+                            Connections {
+                                target: CurrentConversation
+
+                                function onRdvAccountChanged() {
+                                    // This avoid incorrect avatar by always modifying the mode before the imageId
+                                    avatar.mode = CurrentConversation.rdvAccount === CurrentAccount.uri ? Avatar.Mode.Account : Avatar.Mode.Contact
+                                    avatar.imageId = CurrentConversation.rdvAccount === CurrentAccount.uri ? CurrentAccount.id : CurrentConversation.rdvAccount
+                                }
+                            }
+
+                            Avatar {
+                                id: avatar
+                                width: JamiTheme.contactMessageAvatarSize
+                                height: JamiTheme.contactMessageAvatarSize
+                                Layout.leftMargin: JamiTheme.preferredMarginSize
+                                Layout.topMargin: JamiTheme.preferredMarginSize / 2
+                                visible: CurrentConversation.rdvAccount !== ""
+
+                                imageId: ""
+                                showPresenceIndicator: false
+                                mode: Avatar.Mode.Account
+                            }
+
+                            ColumnLayout {
+                                spacing: 0
+                                Layout.alignment: Qt.AlignVCenter
+
+                                ElidedTextLabel {
+                                    id: bestName
+
+                                    eText: {
+                                        if (CurrentConversation.rdvAccount === "")
+                                            return JamiStrings.none
+                                        else if (CurrentConversation.rdvAccount === CurrentAccount.uri)
+                                            return CurrentAccount.bestName
+                                        else
+                                            return UtilsAdapter.getBestNameForUri(CurrentAccount.id, CurrentConversation.rdvAccount)
+                                    }
+                                    maxWidth: JamiTheme.preferredFieldWidth
+
+                                    font.pointSize: JamiTheme.participantFontSize
+                                    color: JamiTheme.primaryForegroundColor
+                                    font.kerning: true
+
+                                    verticalAlignment: Text.AlignVCenter
+                                }
+
+                                ElidedTextLabel {
+                                    id: deviceId
+
+                                    eText: CurrentConversation.rdvDevice === "" ? JamiStrings.none : CurrentConversation.rdvDevice
+                                    visible: CurrentConversation.rdvDevice !== ""
+                                    maxWidth: JamiTheme.preferredFieldWidth
+
+                                    font.pointSize: JamiTheme.participantFontSize
+                                    color: JamiTheme.textColorHovered
+                                    font.kerning: true
+
+                                    horizontalAlignment: Text.AlignRight
+                                    verticalAlignment: Text.AlignVCenter
+                                }
+                            }
+                        }
+                    }
+
+                    TapHandler {
+                        target: parent
+
+                        enabled: parent.visible && root.isAdmin
+                        onTapped: function onTapped(eventPoint) {
+                            devicesListPopup.open()
+                        }
+                    }
+                }
+
                 RowLayout {
                     Layout.leftMargin: JamiTheme.preferredMarginSize
                     Layout.preferredHeight: JamiTheme.settingsFontSize + 2 * JamiTheme.preferredMarginSize + 4
diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp
index 5f8c0ae99..620d5ea5d 100644
--- a/src/app/messagesadapter.cpp
+++ b/src/app/messagesadapter.cpp
@@ -200,6 +200,19 @@ MessagesAdapter::retryInteraction(const QString& interactionId)
         ->retryInteraction(lrcInstance_->get_selectedConvUid(), interactionId);
 }
 
+void
+MessagesAdapter::joinCall(const QString& uri,
+                          const QString& deviceId,
+                          const QString& confId,
+                          bool isAudioOnly)
+{
+    lrcInstance_->getCurrentConversationModel()->joinCall(lrcInstance_->get_selectedConvUid(),
+                                                          uri,
+                                                          deviceId,
+                                                          confId,
+                                                          isAudioOnly);
+}
+
 void
 MessagesAdapter::copyToDownloads(const QString& interactionId, const QString& displayName)
 {
diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h
index 446f8b61e..c8c212879 100644
--- a/src/app/messagesadapter.h
+++ b/src/app/messagesadapter.h
@@ -79,6 +79,10 @@ protected:
     Q_INVOKABLE void openDirectory(const QString& arg);
     Q_INVOKABLE void retryInteraction(const QString& interactionId);
     Q_INVOKABLE void deleteInteraction(const QString& interactionId);
+    Q_INVOKABLE void joinCall(const QString& uri,
+                              const QString& deviceId,
+                              const QString& confId,
+                              bool isAudioOnly = false);
     Q_INVOKABLE void copyToDownloads(const QString& interactionId, const QString& displayName);
     Q_INVOKABLE void userIsComposing(bool isComposing);
     Q_INVOKABLE QVariantMap isLocalImage(const QString& mimeName);
diff --git a/src/app/settingsview/components/TroubleshootSettings.qml b/src/app/settingsview/components/TroubleshootSettings.qml
index d7303ebae..4b9e7937a 100644
--- a/src/app/settingsview/components/TroubleshootSettings.qml
+++ b/src/app/settingsview/components/TroubleshootSettings.qml
@@ -85,4 +85,17 @@ ColumnLayout {
             }
         }
     }
+
+    ToggleSwitch {
+        id: checkboxCallSwarm
+        Layout.fillWidth: true
+        Layout.leftMargin: JamiTheme.preferredMarginSize
+        checked: UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm)
+        labelText: JamiStrings.experimentalCallSwarm
+        fontPointSize: JamiTheme.settingsFontSize
+        tooltipText: JamiStrings.experimentalCallSwarmTooltip
+        onSwitchToggled: {
+            UtilsAdapter.setAppValue(Settings.Key.EnableExperimentalSwarm, checked)
+        }
+    }
 }
diff --git a/src/app/utilsadapter.cpp b/src/app/utilsadapter.cpp
index cc44971d9..bb9d4937c 100644
--- a/src/app/utilsadapter.cpp
+++ b/src/app/utilsadapter.cpp
@@ -371,6 +371,8 @@ UtilsAdapter::setAppValue(const Settings::Key key, const QVariant& value)
         settingsManager_->loadTranslations();
     else if (key == Settings::Key::BaseZoom)
         Q_EMIT changeFontSize();
+    else if (key == Settings::Key::EnableExperimentalSwarm)
+        Q_EMIT showExperimentalCallSwarm();
     else if (key == Settings::Key::ShowChatviewHorizontally)
         Q_EMIT chatviewPositionChanged();
     else if (key == Settings::Key::AppTheme)
diff --git a/src/app/utilsadapter.h b/src/app/utilsadapter.h
index 7a239c20b..dd2b2ac92 100644
--- a/src/app/utilsadapter.h
+++ b/src/app/utilsadapter.h
@@ -126,6 +126,7 @@ Q_SIGNALS:
     void changeFontSize();
     void chatviewPositionChanged();
     void appThemeChanged();
+    void showExperimentalCallSwarm();
 
 private:
     QClipboard* clipboard_;
diff --git a/src/libclient/api/conversation.h b/src/libclient/api/conversation.h
index 5cb87ecc0..2671b7a1c 100644
--- a/src/libclient/api/conversation.h
+++ b/src/libclient/api/conversation.h
@@ -71,6 +71,9 @@ struct Info
     QString uid = "";
     QString accountId;
     QVector<member::Member> participants;
+    VectorMapStringString activeCalls;
+    VectorMapStringString ignoredActiveCalls;
+
     QString callId;
     QString confId;
     std::unique_ptr<MessageListModel> interactions;
@@ -84,6 +87,18 @@ struct Info
     MapStringString infos {};
     MapStringString preferences {};
 
+    int indexOfActiveCall(const MapStringString& commit)
+    {
+        for (auto idx = 0; idx != activeCalls.size(); ++idx) {
+            const auto& call = activeCalls[idx];
+            if (call["id"] == commit["confId"] && call["uri"] == commit["uri"]
+                && call["device"] == commit["device"]) {
+                return idx;
+            }
+        }
+        return -1;
+    }
+
     QString getCallId() const
     {
         return confId.isEmpty() ? callId : confId;
diff --git a/src/libclient/api/conversationmodel.h b/src/libclient/api/conversationmodel.h
index 13e158431..8106ba645 100644
--- a/src/libclient/api/conversationmodel.h
+++ b/src/libclient/api/conversationmodel.h
@@ -210,6 +210,11 @@ public:
      * @param uid of the conversation
      */
     void placeAudioOnlyCall(const QString& uid);
+    void joinCall(const QString& uid,
+                  const QString& confId,
+                  const QString& uri,
+                  const QString& deviceId,
+                  bool isAudioOnly);
     /**
      * Send a message to the conversation
      * @param uid of the conversation
@@ -378,6 +383,17 @@ public:
      * @param conversationId
      */
     void popFrontError(const QString& conversationId);
+    /**
+     * Ignore an active call
+     * @param convId
+     * @param id
+     * @param uri
+     * @param device
+     */
+    void ignoreActiveCall(const QString& convId,
+                          const QString& id,
+                          const QString& uri,
+                          const QString& device);
 
     /**
      * @return if conversations requests exists.
@@ -432,15 +448,6 @@ Q_SIGNALS:
     void newInteraction(const QString& uid,
                         QString& interactionId,
                         const interaction::Info& interactionInfo) const;
-    /**
-     * Emitted when an interaction got a new status
-     * @param convUid conversation which owns the interaction
-     * @param interactionId
-     * @param msg
-     */
-    void interactionStatusUpdated(const QString& convUid,
-                                  const QString& interactionId,
-                                  const api::interaction::Info& msg) const;
     /**
      * Emitted when an interaction got removed from the conversation
      * @param convUid conversation which owns the interaction
@@ -546,6 +553,11 @@ Q_SIGNALS:
      */
     void newMessagesAvailable(const QString& accountId, const QString& conversationId) const;
 
+    /**
+     * Emitted whenever conversation's calls changed
+     */
+    void activeCallsChanged(const QString& accountId, const QString& conversationId) const;
+
     /**
      * Emitted when creation of conversation started, finished with success or finisfed with error
      * @param accountId  account id
@@ -598,6 +610,11 @@ Q_SIGNALS:
     void messagesFoundProcessed(const QString& accountId,
                                 const VectorMapStringString& messageIds,
                                 const QVector<interaction::Info>& messageInformations) const;
+    /**
+     * Emitted once a conversation needs somebody to host the call
+     * @param callId
+     */
+    void needsHost(const QString& conversationId) const;
 
 private:
     std::unique_ptr<ConversationModelPimpl> pimpl_;
diff --git a/src/libclient/api/interaction.h b/src/libclient/api/interaction.h
index 5993fc79e..54b22fb6e 100644
--- a/src/libclient/api/interaction.h
+++ b/src/libclient/api/interaction.h
@@ -272,6 +272,7 @@ struct Info
     QString authorUri;
     QString body;
     QString parentId = "";
+    QString confId;
     std::time_t timestamp = 0;
     std::time_t duration = 0;
     Type type = Type::INVALID;
@@ -318,6 +319,8 @@ struct Info
             body = QObject::tr("Swarm created");
         } else if (type == Type::CALL) {
             duration = message["duration"].toInt() / 1000;
+            if (message.contains("confId"))
+                confId = message["confId"];
         }
         commit = message;
     }
diff --git a/src/libclient/authority/storagehelper.cpp b/src/libclient/authority/storagehelper.cpp
index 1425a49da..c17364c8b 100644
--- a/src/libclient/authority/storagehelper.cpp
+++ b/src/libclient/authority/storagehelper.cpp
@@ -167,7 +167,7 @@ getFormattedCallDuration(const std::time_t duration)
 }
 
 QString
-getCallInteractionString(const QString& authorUri, const std::time_t& duration)
+getCallInteractionStringNonSwarm(const QString& authorUri, const std::time_t& duration)
 {
     if (duration < 0) {
         if (authorUri.isEmpty()) {
@@ -190,6 +190,17 @@ getCallInteractionString(const QString& authorUri, const std::time_t& duration)
     }
 }
 
+QString
+getCallInteractionString(const api::interaction::Info& info)
+{
+    if (!info.confId.isEmpty()) {
+        if (info.duration <= 0) {
+            return QObject::tr("Join call");
+        }
+    }
+    return getCallInteractionStringNonSwarm(info.authorUri, info.duration);
+}
+
 QString
 getContactInteractionString(const QString& authorUri, const api::interaction::Status& status)
 {
@@ -510,7 +521,7 @@ getHistory(Database& db, api::conversation::Info& conversation)
                                        : std::stoi(durationString.toStdString());
             auto status = api::interaction::to_status(payloads[i + 5]);
             if (type == api::interaction::Type::CALL) {
-                body = getCallInteractionString(payloads[i + 1], duration);
+                body = getCallInteractionStringNonSwarm(payloads[i + 1], duration);
             } else if (type == api::interaction::Type::CONTACT) {
                 body = getContactInteractionString(payloads[i + 1], status);
             }
diff --git a/src/libclient/authority/storagehelper.h b/src/libclient/authority/storagehelper.h
index 2dc68a315..4721f17c9 100644
--- a/src/libclient/authority/storagehelper.h
+++ b/src/libclient/authority/storagehelper.h
@@ -56,11 +56,11 @@ QString prepareUri(const QString& uri, api::profile::Type type);
 
 /**
  * Get a formatted string for a call interaction's body
- * @param author_uri
- * @param duration of the call
+ * @param info
  * @return the formatted and translated call message string
  */
-QString getCallInteractionString(const QString& authorUri, const std::time_t& duration);
+QString getCallInteractionString(const api::interaction::Info& info);
+QString getCallInteractionStringNonSwarm(const QString& authorUri, const std::time_t& duration);
 
 /**
  * Get a formatted string for a contact interaction's body
diff --git a/src/libclient/callbackshandler.cpp b/src/libclient/callbackshandler.cpp
index ab3949b35..76e07a542 100644
--- a/src/libclient/callbackshandler.cpp
+++ b/src/libclient/callbackshandler.cpp
@@ -128,6 +128,12 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent)
             &CallbacksHandler::slotAccountMessageStatusChanged,
             Qt::QueuedConnection);
 
+    connect(&ConfigurationManager::instance(),
+            &ConfigurationManagerInterface::needsHost,
+            this,
+            &CallbacksHandler::slotNeedsHost,
+            Qt::QueuedConnection);
+
     connect(&ConfigurationManager::instance(),
             &ConfigurationManagerInterface::accountDetailsChanged,
             this,
@@ -349,6 +355,11 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent)
             this,
             &CallbacksHandler::slotOnConversationError,
             Qt::QueuedConnection);
+    connect(&ConfigurationManager::instance(),
+            &ConfigurationManagerInterface::activeCallsChanged,
+            this,
+            &CallbacksHandler::slotActiveCallsChanged,
+            Qt::QueuedConnection);
     connect(&ConfigurationManager::instance(),
             &ConfigurationManagerInterface::conversationPreferencesUpdated,
             this,
@@ -576,6 +587,7 @@ CallbacksHandler::slotConferenceChanged(const QString& accountId,
                                         const QString& callId,
                                         const QString& state)
 {
+    Q_EMIT conferenceChanged(accountId, callId, state);
     slotCallStateChanged(accountId, callId, state, 0);
 }
 
@@ -595,6 +607,12 @@ CallbacksHandler::slotAccountMessageStatusChanged(const QString& accountId,
     Q_EMIT accountMessageStatusChanged(accountId, conversationId, peer, messageId, status);
 }
 
+void
+CallbacksHandler::slotNeedsHost(const QString& accountId, const QString& conversationId)
+{
+    Q_EMIT needsHost(accountId, conversationId);
+}
+
 void
 CallbacksHandler::slotDataTransferEvent(const QString& accountId,
                                         const QString& conversationId,
@@ -823,6 +841,14 @@ CallbacksHandler::slotOnConversationError(const QString& accountId,
     Q_EMIT conversationError(accountId, conversationId, code, what);
 }
 
+void
+CallbacksHandler::slotActiveCallsChanged(const QString& accountId,
+                                         const QString& conversationId,
+                                         const VectorMapStringString& activeCalls)
+{
+    Q_EMIT activeCallsChanged(accountId, conversationId, activeCalls);
+}
+
 void
 CallbacksHandler::slotConversationPreferencesUpdated(const QString& accountId,
                                                      const QString& conversationId,
diff --git a/src/libclient/callbackshandler.h b/src/libclient/callbackshandler.h
index 4f3ad2711..0466f7ec9 100644
--- a/src/libclient/callbackshandler.h
+++ b/src/libclient/callbackshandler.h
@@ -195,12 +195,19 @@ Q_SIGNALS:
      * @param callId of the conference
      */
     void conferenceCreated(const QString& accountId, const QString& callId);
+    void conferenceChanged(const QString& accountId, const QString& confId, const QString& state);
     /**
      * Connect this signal to know when a conference is removed
      * @param accountId
      * @param callId of the conference
      */
     void conferenceRemoved(const QString& accountId, const QString& callId);
+    /**
+     * Connect this signal to know if a conversation needs an host.
+     * @param accountId, account linked
+     * @param conversationId id of the conversation
+     */
+    void needsHost(const QString& accountId, const QString& conversationId);
     /**
      * Connect this signal to know when a message sent get a new status
      * @param accountId, account linked
@@ -374,6 +381,9 @@ Q_SIGNALS:
                            const QString& conversationId,
                            int code,
                            const QString& what);
+    void activeCallsChanged(const QString& accountId,
+                            const QString& conversationId,
+                            const VectorMapStringString& activeCalls);
     void conversationPreferencesUpdated(const QString& accountId,
                                         const QString& conversationId,
                                         const MapStringString& preferences);
@@ -549,6 +559,12 @@ private Q_SLOTS:
                                          const QString& peer,
                                          const QString& messageId,
                                          int status);
+    /**
+     * Emit needsHost
+     * @param accountId, account linked
+     * @param conversationId id of the conversation
+     */
+    void slotNeedsHost(const QString& accountId, const QString& conversationId);
 
     void slotDataTransferEvent(const QString& accountId,
                                const QString& conversationId,
@@ -699,6 +715,9 @@ private Q_SLOTS:
                                  const QString& conversationId,
                                  int code,
                                  const QString& what);
+    void slotActiveCallsChanged(const QString& accountId,
+                                const QString& conversationId,
+                                const VectorMapStringString& activeCalls);
 
 private:
     const api::Lrc& parent;
diff --git a/src/libclient/callmodel.cpp b/src/libclient/callmodel.cpp
index e5fe8c8a2..354d67233 100644
--- a/src/libclient/callmodel.cpp
+++ b/src/libclient/callmodel.cpp
@@ -239,6 +239,9 @@ public Q_SLOTS:
      * @param callId
      */
     void slotConferenceCreated(const QString& accountId, const QString& callId);
+    void slotConferenceChanged(const QString& accountId,
+                               const QString& callId,
+                               const QString& state);
     /**
      * Listen from CallbacksHandler when a voice mail notice is incoming
      * @param accountId
@@ -409,6 +412,23 @@ CallModel::getAdvancedInformation() const
     return pimpl_->callAdvancedInformation();
 }
 
+void
+CallModel::emplaceConversationConference(const QString& confId)
+{
+    if (hasCall(confId))
+        return;
+
+    auto callInfo = std::make_shared<call::Info>();
+    callInfo->id = confId;
+    callInfo->isOutgoing = false;
+    callInfo->status = call::Status::SEARCHING;
+    callInfo->type = call::Type::CONFERENCE;
+    callInfo->isAudioOnly = false;
+    callInfo->videoMuted = false;
+    callInfo->mediaList = {};
+    pimpl_->calls.emplace(confId, std::move(callInfo));
+}
+
 void
 CallModel::muteMedia(const QString& callId, const QString& label, bool mute)
 {
@@ -940,6 +960,10 @@ CallModelPimpl::CallModelPimpl(const CallModel& linked,
             &CallbacksHandler::conferenceCreated,
             this,
             &CallModelPimpl::slotConferenceCreated);
+    connect(&callbacksHandler,
+            &CallbacksHandler::conferenceChanged,
+            this,
+            &CallModelPimpl::slotConferenceChanged);
     connect(&callbacksHandler,
             &CallbacksHandler::voiceMailNotify,
             this,
@@ -1566,9 +1590,13 @@ CallModelPimpl::slotOnConferenceInfosUpdated(const QString& confId,
     QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId);
     Q_FOREACH (const auto& call, callList) {
         Q_EMIT linked.callAddedToConference(call, confId);
-        calls[call]->videoMuted = it->second->videoMuted;
-        calls[call]->audioMuted = it->second->audioMuted;
-        Q_EMIT linked.callInfosChanged(linked.owner.id, call);
+        if (calls.find(call) == calls.end()) {
+            qWarning() << "Call not found";
+        } else {
+            calls[call]->videoMuted = it->second->videoMuted;
+            calls[call]->audioMuted = it->second->audioMuted;
+            Q_EMIT linked.callInfosChanged(linked.owner.id, call);
+        }
     }
     Q_EMIT linked.callInfosChanged(linked.owner.id, confId);
     Q_EMIT linked.onParticipantsChanged(confId);
@@ -1585,14 +1613,7 @@ CallModelPimpl::slotConferenceCreated(const QString& accountId, const QString& c
 {
     if (accountId != linked.owner.id)
         return;
-    // Detect if conference is created for this account
     QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId);
-    auto hasConference = false;
-    Q_FOREACH (const auto& call, callList) {
-        hasConference |= linked.hasCall(call);
-    }
-    if (!hasConference)
-        return;
 
     auto callInfo = std::make_shared<call::Info>();
     callInfo->id = confId;
@@ -1625,6 +1646,20 @@ CallModelPimpl::slotConferenceCreated(const QString& accountId, const QString& c
     }
 }
 
+void
+CallModelPimpl::slotConferenceChanged(const QString& accountId,
+                                      const QString& confId,
+                                      const QString& state)
+{
+    if (accountId != linked.owner.id)
+        return;
+    // Detect if conference is created for this account
+    QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId);
+    Q_FOREACH (const auto& call, callList) {
+        Q_EMIT linked.callAddedToConference(call, confId);
+    }
+}
+
 void
 CallModelPimpl::sendProfile(const QString& callId)
 {
diff --git a/src/libclient/conversationmodel.cpp b/src/libclient/conversationmodel.cpp
index 2e1d7b471..23b9bc558 100644
--- a/src/libclient/conversationmodel.cpp
+++ b/src/libclient/conversationmodel.cpp
@@ -193,10 +193,7 @@ public:
     /**
      * Handle data transfer progression
      */
-    void updateTransferProgress(QTimer* timer,
-                                const QString& conversation,
-                                int conversationIdx,
-                                const QString& interactionId);
+    void updateTransferProgress(QTimer* timer, int conversationIdx, const QString& interactionId);
 
     bool usefulDataFromDataTransfer(const QString& fileId,
                                     const datatransfer::Info& info,
@@ -385,6 +382,9 @@ public Q_SLOTS:
                                  const QString& conversationId,
                                  int code,
                                  const QString& what);
+    void slotActiveCallsChanged(const QString& accountId,
+                                const QString& conversationId,
+                                const VectorMapStringString& activeCalls);
     void slotConversationReady(const QString& accountId, const QString& conversationId);
     void slotConversationRemoved(const QString& accountId, const QString& conversationId);
     void slotConversationPreferencesUpdated(const QString& accountId,
@@ -853,6 +853,30 @@ ConversationModel::deleteObsoleteHistory(int days)
     storage::deleteObsoleteHistory(pimpl_->db, date);
 }
 
+void
+ConversationModel::joinCall(const QString& uid,
+                            const QString& uri,
+                            const QString& deviceId,
+                            const QString& confId,
+                            bool isAudioOnly)
+{
+    try {
+        auto& conversation = pimpl_->getConversationForUid(uid, true).get();
+        if (!conversation.callId.isEmpty()) {
+            qWarning() << "Already in a call for swarm:" + uid;
+            return;
+        }
+        conversation.callId = owner.callModel->createCall("rdv:" + uid + "/" + uri + "/" + deviceId
+                                                              + "/" + confId,
+                                                          isAudioOnly);
+        // Update interaction status
+        pimpl_->invalidateModel();
+        emit selectConversation(uid);
+        emit conversationUpdated(uid);
+    } catch (...) {
+    }
+}
+
 void
 ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly)
 {
@@ -864,6 +888,19 @@ ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly)
                 << "ConversationModel::placeCall can't call a conversation without participant";
             return;
         }
+
+        if (!conversation.isCoreDialog() && conversation.isSwarm()) {
+            qDebug() << "Start call for swarm:" + uid;
+            conversation.callId = linked.owner.callModel->createCall("swarm:" + uid, isAudioOnly);
+
+            // Update interaction status
+            invalidateModel();
+            emit linked.selectConversation(conversation.uid);
+            emit linked.conversationUpdated(conversation.uid);
+            Q_EMIT linked.dataChanged(indexOf(conversation.uid));
+            return;
+        }
+
         auto& peers = peersForConversation(conversation);
         // there is no calls in group with more than 2 participants
         if (peers.size() != 1) {
@@ -1028,6 +1065,25 @@ ConversationModel::popFrontError(const QString& conversationId)
     Q_EMIT onConversationErrorsUpdated(conversationId);
 }
 
+void
+ConversationModel::ignoreActiveCall(const QString& conversationId,
+                                    const QString& id,
+                                    const QString& uri,
+                                    const QString& device)
+{
+    auto conversationOpt = getConversationForUid(conversationId);
+    if (!conversationOpt.has_value())
+        return;
+
+    auto& conversation = conversationOpt->get();
+    MapStringString mapCall;
+    mapCall["id"] = id;
+    mapCall["uri"] = uri;
+    mapCall["device"] = device;
+    conversation.ignoredActiveCalls.push_back(mapCall);
+    Q_EMIT activeCallsChanged(owner.id, conversationId);
+}
+
 void
 ConversationModel::setConversationPreferences(const QString& conversationId,
                                               const MapStringString prefs)
@@ -1835,6 +1891,9 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
             &ConfigurationManagerInterface::composingStatusChanged,
             this,
             &ConversationModelPimpl::slotComposingStatusChanged);
+    connect(&callbacksHandler, &CallbacksHandler::needsHost, this, [&](auto, auto convId) {
+        emit linked.needsHost(convId);
+    });
 
     // data transfer
     connect(&*linked.owner.contactModel,
@@ -1918,6 +1977,10 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
             &CallbacksHandler::conversationPreferencesUpdated,
             this,
             &ConversationModelPimpl::slotConversationPreferencesUpdated);
+    connect(&callbacksHandler,
+            &CallbacksHandler::activeCallsChanged,
+            this,
+            &ConversationModelPimpl::slotActiveCallsChanged);
 }
 
 ConversationModelPimpl::~ConversationModelPimpl()
@@ -2066,6 +2129,10 @@ ConversationModelPimpl::~ConversationModelPimpl()
                &CallbacksHandler::conversationError,
                this,
                &ConversationModelPimpl::slotOnConversationError);
+    disconnect(&callbacksHandler,
+               &CallbacksHandler::activeCallsChanged,
+               this,
+               &ConversationModelPimpl::slotActiveCallsChanged);
     disconnect(&callbacksHandler,
                &CallbacksHandler::conversationPreferencesUpdated,
                this,
@@ -2383,7 +2450,7 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId,
                 linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
                 downloadFile = (bytesProgress == 0);
             } else if (msg.type == interaction::Type::CALL) {
-                msg.body = storage::getCallInteractionString(msg.authorUri, msg.duration);
+                msg.body = storage::getCallInteractionString(msg);
             } else if (msg.type == interaction::Type::CONTACT) {
                 auto bestName = msg.authorUri == linked.owner.profileInfo.uri
                                     ? linked.owner.accountModel->bestNameForAccount(linked.owner.id)
@@ -2498,6 +2565,8 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId,
         api::datatransfer::Info info;
         QString fileId;
 
+        auto updateUnread = false;
+
         if (msg.type == interaction::Type::DATA_TRANSFER) {
             // save data transfer interaction to db and assosiate daemon id with interaction id,
             // conversation id and db id
@@ -2520,8 +2589,14 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId,
                          : bytesProgress == totalSize ? interaction::Status::TRANSFER_FINISHED
                                                       : interaction::Status::TRANSFER_ONGOING;
             linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
+            if (msg.authorUri != linked.owner.profileInfo.uri) {
+                updateUnread = true;
+            }
         } else if (msg.type == interaction::Type::CALL) {
-            msg.body = storage::getCallInteractionString(msg.authorUri, msg.duration);
+            // If we're a call in a swarm
+            if (msg.authorUri != linked.owner.profileInfo.uri)
+                updateUnread = true;
+            msg.body = storage::getCallInteractionString(msg);
         } else if (msg.type == interaction::Type::CONTACT) {
             auto bestName = msg.authorUri == linked.owner.profileInfo.uri
                                 ? linked.owner.accountModel->bestNameForAccount(linked.owner.id)
@@ -2529,16 +2604,24 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId,
             msg.body = interaction::getContactInteractionString(bestName,
                                                                 interaction::to_action(
                                                                     message["action"]));
-        } else if (msg.type == interaction::Type::TEXT
-                   && msg.authorUri != linked.owner.profileInfo.uri) {
-            conversation.unreadMessages++;
+            if (msg.authorUri != linked.owner.profileInfo.uri) {
+                updateUnread = true;
+            }
+        } else if (msg.type == interaction::Type::TEXT) {
+            if (msg.authorUri != linked.owner.profileInfo.uri) {
+                updateUnread = true;
+            }
         } else if (msg.type == interaction::Type::EDITED) {
             conversation.interactions->addEdition(msgId, msg, true);
         }
+
         if (!insertSwarmInteraction(msgId, msg, conversation, false)) {
             // message already exists
             return;
         }
+        if (updateUnread) {
+            conversation.unreadMessages++;
+        }
         if (msg.type == interaction::Type::MERGE) {
             invalidateModel();
             return;
@@ -2811,6 +2894,24 @@ ConversationModelPimpl::slotOnConversationError(const QString& accountId,
     }
 }
 
+void
+ConversationModelPimpl::slotActiveCallsChanged(const QString& accountId,
+                                               const QString& conversationId,
+                                               const VectorMapStringString& activeCalls)
+{
+    if (accountId != linked.owner.id || indexOf(conversationId) < 0) {
+        return;
+    }
+    try {
+        auto& conversation = getConversationForUid(conversationId).get();
+        conversation.activeCalls = activeCalls;
+        if (activeCalls.empty())
+            conversation.ignoredActiveCalls.clear();
+        Q_EMIT linked.activeCallsChanged(accountId, conversationId);
+    } catch (...) {
+    }
+}
+
 void
 ConversationModelPimpl::slotIncomingContactRequest(const QString& contactUri)
 {
@@ -3074,6 +3175,9 @@ ConversationModelPimpl::addSwarmConversation(const QString& convId)
     conversation.infos = details;
     conversation.uid = convId;
     conversation.accountId = linked.owner.id;
+    VectorMapStringString activeCalls = ConfigurationManager::instance()
+                                            .getActiveCalls(linked.owner.id, convId);
+    conversation.activeCalls = activeCalls;
     QString lastRead;
     VectorString membersLeft;
     for (auto& member : members) {
@@ -3387,12 +3491,13 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
                                                bool incoming,
                                                const std::time_t& duration)
 {
-    // do not save call interaction for swarm conversation
-    try {
-        auto& conv = getConversationForPeerUri(from).get();
-        if (conv.isSwarm())
-            return;
-    } catch (const std::exception&) {
+    // Get conversation
+    auto conv_it = std::find_if(conversations.begin(),
+                                conversations.end(),
+                                [&callId](const conversation::Info& conversation) {
+                                    return conversation.callId == callId;
+                                });
+    if (conv_it == conversations.end()) {
         // If we have no conversation with peer.
         try {
             auto contact = linked.owner.contactModel->getContact(from);
@@ -3401,18 +3506,18 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
                 storage::beginConversationWithPeer(db, contact.profileInfo.uri);
             }
         } catch (const std::exception&) {
+            return;
+        }
+        try {
+            auto& conv = getConversationForPeerUri(from).get();
+            conv.callId = callId;
+        } catch (...) {
+            return;
         }
     }
-
-    // Get conversation
-    auto conv_it = std::find_if(conversations.begin(),
-                                conversations.end(),
-                                [&callId](const conversation::Info& conversation) {
-                                    return conversation.callId == callId;
-                                });
-    if (conv_it == conversations.end()) {
+    // do not save call interaction for swarm conversation
+    if (conv_it->isSwarm())
         return;
-    }
     auto uid = conv_it->uid;
     auto uriString = incoming ? storage::prepareUri(from, linked.owner.profileInfo.type) : "";
     auto msg = interaction::Info {uriString,
@@ -3425,7 +3530,7 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
     // update the db
     auto msgId = storage::addOrUpdateMessage(db, conv_it->uid, msg, callId);
     // now set the formatted call message string in memory only
-    msg.body = storage::getCallInteractionString(uriString, duration);
+    msg.body = storage::getCallInteractionString(msg);
     bool newInteraction = false;
     {
         std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]);
@@ -3562,6 +3667,7 @@ ConversationModelPimpl::slotCallAddedToConference(const QString& callId, const Q
                                               .getConferenceDetails(linked.owner.id, confId);
             if (confDetails["STATE"] == "ACTIVE_ATTACHED")
                 Q_EMIT linked.selectConversation(conversation.uid);
+            return;
         }
     }
 }
@@ -3693,7 +3799,7 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId,
                 if (peerId != linked.owner.profileInfo.uri)
                     conversation.interactions->setRead(peerId, messageId);
                 else {
-                    // Here, this means that the daemon synched the displayed message
+                    // Here, this means that the daemon synced the displayed message
                     // so, compute the number of unread messages.
                     conversation.unreadMessages = ConfigurationManager::instance()
                                                       .countInteractions(linked.owner.id,
@@ -4150,7 +4256,7 @@ ConversationModelPimpl::slotTransferStatusOngoing(const QString& fileId, datatra
     auto conversationIdx = indexOf(conversationId);
     auto* timer = new QTimer();
     connect(timer, &QTimer::timeout, [=] {
-        updateTransferProgress(timer, conversationId, conversationIdx, interactionId);
+        updateTransferProgress(timer, conversationIdx, interactionId);
     });
     timer->start(1000);
 }
@@ -4281,7 +4387,6 @@ ConversationModelPimpl::updateTransferStatus(const QString& fileId,
 
 void
 ConversationModelPimpl::updateTransferProgress(QTimer* timer,
-                                               const QString&,
                                                int conversationIdx,
                                                const QString& interactionId)
 {
diff --git a/src/libclient/messagelistmodel.cpp b/src/libclient/messagelistmodel.cpp
index e96a258b6..e7013b998 100644
--- a/src/libclient/messagelistmodel.cpp
+++ b/src/libclient/messagelistmodel.cpp
@@ -68,6 +68,20 @@ MessageListModel::find(const QString& msgId)
     return interactions_.end();
 }
 
+iterator
+MessageListModel::findActiveCall(const MapStringString& commit)
+{
+    iterator it;
+    for (it = interactions_.begin(); it != interactions_.end(); ++it) {
+        const auto& itCommit = it->second.commit;
+        if (itCommit["confId"] == commit["confId"] && itCommit["uri"] == commit["uri"]
+            && itCommit["device"] == commit["device"]) {
+            return it;
+        }
+    }
+    return interactions_.end();
+}
+
 iterator
 MessageListModel::erase(const iterator& it)
 {
@@ -403,6 +417,13 @@ MessageListModel::dataForItem(item_t item, int, int role) const
     case Role::Timestamp:
         return QVariant::fromValue(item.second.timestamp);
     case Role::Duration:
+        if (!item.second.commit.empty()) {
+            // For swarm, check the commit value
+            if (item.second.commit.find("duration") == item.second.commit.end())
+                return QVariant::fromValue(0);
+            else
+                return QVariant::fromValue(item.second.commit["duration"].toInt() / 1000);
+        }
         return QVariant::fromValue(item.second.duration);
     case Role::Type:
         return QVariant(static_cast<int>(item.second.type));
@@ -416,6 +437,10 @@ MessageListModel::dataForItem(item_t item, int, int role) const
         return QVariant(item.second.linkified);
     case Role::ActionUri:
         return QVariant(item.second.commit["uri"]);
+    case Role::ConfId:
+        return QVariant(item.second.commit["confId"]);
+    case Role::DeviceId:
+        return QVariant(item.second.commit["device"]);
     case Role::ContactAction:
         return QVariant(item.second.commit["action"]);
     case Role::PreviousBodies: {
diff --git a/src/libclient/messagelistmodel.h b/src/libclient/messagelistmodel.h
index fbbf3936f..f5b662bf4 100644
--- a/src/libclient/messagelistmodel.h
+++ b/src/libclient/messagelistmodel.h
@@ -43,6 +43,8 @@ struct Info;
     X(IsRead) \
     X(ContactAction) \
     X(ActionUri) \
+    X(ConfId) \
+    X(DeviceId) \
     X(LinkPreviewInfo) \
     X(Linkified) \
     X(PreviousBodies) \
@@ -84,7 +86,9 @@ public:
                                   interaction::Info message,
                                   bool beginning = false);
     iterator find(const QString& msgId);
+    iterator findActiveCall(const MapStringString& commit);
     iterator erase(const iterator& it);
+
     constIterator find(const QString& msgId) const;
     QPair<iterator, bool> insert(std::pair<QString, interaction::Info> message,
                                  bool beginning = false);
diff --git a/src/libclient/qtwrapper/configurationmanager_wrap.h b/src/libclient/qtwrapper/configurationmanager_wrap.h
index b3690d695..e66bc93d6 100644
--- a/src/libclient/qtwrapper/configurationmanager_wrap.h
+++ b/src/libclient/qtwrapper/configurationmanager_wrap.h
@@ -92,8 +92,6 @@ public:
                     Q_EMIT this->volatileAccountDetailsChanged(QString(accountID.c_str()),
                                                                convertMap(details));
                 }),
-            exportable_callback<ConfigurationSignal::Error>(
-                [this](int code) { Q_EMIT this->errorAlert(code); }),
             exportable_callback<ConfigurationSignal::CertificateExpired>(
                 [this](const std::string& certId) {
                     Q_EMIT this->certificateExpired(QString(certId.c_str()));
@@ -129,6 +127,11 @@ public:
                                                              QString(message_id.c_str()),
                                                              state);
                 }),
+            exportable_callback<libjami::ConfigurationSignal::NeedsHost>(
+                [this](const std::string& account_id, const std::string& conversation_id) {
+                    Q_EMIT this->needsHost(QString(account_id.c_str()),
+                                           QString(conversation_id.c_str()));
+                }),
             exportable_callback<ConfigurationSignal::IncomingTrustRequest>(
                 [this](const std::string& accountId,
                        const std::string& conversationId,
@@ -356,6 +359,14 @@ public:
                                                   QString(conversationId.c_str()),
                                                   code,
                                                   QString(what.c_str()));
+                   }),
+               exportable_callback<ConfigurationSignal::ActiveCallsChanged>(
+                   [this](const std::string& accountId,
+                          const std::string& conversationId,
+                          const std::vector<std::map<std::string, std::string>>& activeCalls) {
+                       Q_EMIT activeCallsChanged(QString(accountId.c_str()),
+                                                 QString(conversationId.c_str()),
+                                                 convertVecMap(activeCalls));
                    })};
     }
 
@@ -432,7 +443,16 @@ public Q_SLOTS: // METHODS
 
     QStringList getAccountList()
     {
-        QStringList temp = convertStringList(libjami::getAccountList());
+        return convertStringList(libjami::getAccountList());
+    }
+
+    VectorMapStringString getActiveCalls(const QString& accountId, const QString& convId)
+    {
+        VectorMapStringString temp;
+        for (const auto& x :
+             libjami::getActiveCalls(accountId.toStdString(), convId.toStdString())) {
+            temp.push_back(convertMap(x));
+        }
         return temp;
     }
 
@@ -1195,7 +1215,6 @@ Q_SIGNALS: // SIGNALS
                                   unsigned detail_code,
                                   const QString& detail_str);
     void stunStatusSuccess(const QString& message);
-    void errorAlert(int code);
     void volatileAccountDetailsChanged(const QString& accountID, MapStringString details);
     void certificatePinned(const QString& certId);
     void certificatePathPinned(const QString& path, const QStringList& certIds);
@@ -1222,6 +1241,7 @@ Q_SIGNALS: // SIGNALS
                                      const QString& peer,
                                      const QString& messageId,
                                      int status);
+    void needsHost(const QString& accountId, const QString& conversationId);
     void nameRegistrationEnded(const QString& accountId, int status, const QString& name);
     void registeredNameFound(const QString& accountId,
                              int status,
@@ -1278,6 +1298,9 @@ Q_SIGNALS: // SIGNALS
                              const QString& conversationId,
                              int code,
                              const QString& what);
+    void activeCallsChanged(const QString& accountId,
+                            const QString& conversationId,
+                            const VectorMapStringString& activeCalls);
     void conversationPreferencesUpdated(const QString& accountId,
                                         const QString& conversationId,
                                         const MapStringString& message);
-- 
GitLab