From 77aae84786f64956bb95d4b880832b0a30267c47 Mon Sep 17 00:00:00 2001
From: Ming Rui Zhang <mingrui.zhang@savoirfairelinux.com>
Date: Mon, 17 May 2021 11:06:11 -0400
Subject: [PATCH] call: add call status indicator when adding new participants
 into a conference

Gitlab: #410

Change-Id: Iff3b06b123363478794fd7e419db3d2d0ae10bb7
---
 images/icons/cross_black_24dp.svg             |   8 +
 qml.qrc                                       |   2 +
 resources.qrc                                 |   1 +
 src/calladapter.cpp                           |  24 +--
 src/calladapter.h                             |   2 +-
 src/calloverlaymodel.cpp                      | 128 ++++++++++++
 src/calloverlaymodel.h                        |  41 ++++
 src/commoncomponents/AvatarImage.qml          | 193 +++++++++---------
 src/commoncomponents/PhotoboothView.qml       |  20 +-
 src/commoncomponents/SpinningAnimation.qml    |  83 +++++++-
 src/constant/JamiTheme.qml                    |  13 ++
 .../components/ContactPickerItemDelegate.qml  |   2 +-
 src/mainview/components/InitialCallPage.qml   |  10 +-
 src/mainview/components/MainOverlay.qml       |  12 +-
 src/mainview/components/OngoingCallPage.qml   |   2 +-
 .../ParticipantCallInStatusDelegate.qml       | 141 +++++++++++++
 .../ParticipantCallInStatusView.qml           |  60 ++++++
 .../components/ParticipantOverlay.qml         |  10 +-
 .../components/SmartListItemDelegate.qml      |   2 +-
 src/mainview/components/UserProfile.qml       |   2 +-
 .../components/ContactItemDelegate.qml        |   2 +-
 src/wizardview/components/ProfilePage.qml     |   6 +-
 22 files changed, 610 insertions(+), 154 deletions(-)
 create mode 100644 images/icons/cross_black_24dp.svg
 create mode 100644 src/mainview/components/ParticipantCallInStatusDelegate.qml
 create mode 100644 src/mainview/components/ParticipantCallInStatusView.qml

diff --git a/images/icons/cross_black_24dp.svg b/images/icons/cross_black_24dp.svg
new file mode 100644
index 000000000..3e5b5f55b
--- /dev/null
+++ b/images/icons/cross_black_24dp.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<path d="M13.3,12.4l2.1-2.1c0.3-0.3,0.3-0.8,0-1.1c-0.3-0.3-0.8-0.3-1.1,0l-2.1,2.1l-2.1-2.1C9.8,8.9,9.3,8.9,9,9.2
+	C8.9,9.3,8.8,9.5,8.8,9.7c0,0.2,0.1,0.4,0.2,0.6l2.1,2.1L9,14.5c-0.2,0.1-0.2,0.3-0.2,0.5c0,0.2,0.1,0.4,0.2,0.6
+	c0.3,0.3,0.8,0.3,1.1,0l2.1-2.1l2.1,2.1c0.1,0.1,0.3,0.2,0.5,0.2c0.2,0,0.4-0.1,0.5-0.2c0.3-0.3,0.3-0.8,0-1.1L13.3,12.4z"/>
+</svg>
diff --git a/qml.qrc b/qml.qrc
index 80b9a37d1..b9c2e3bbe 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -141,5 +141,7 @@
         <file>src/mainview/components/CallActionBar.qml</file>
         <file>src/commoncomponents/HalfPill.qml</file>
         <file>src/commoncomponents/MaterialToolTip.qml</file>
+        <file>src/mainview/components/ParticipantCallInStatusDelegate.qml</file>
+        <file>src/mainview/components/ParticipantCallInStatusView.qml</file>
     </qresource>
 </RCC>
diff --git a/resources.qrc b/resources.qrc
index bbfe9d506..e5e4217a5 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -5,6 +5,7 @@
         <file>images/icons/baseline-refresh-24px.svg</file>
         <file>images/jami_rolling_spinner.gif</file>
         <file>images/icons/baseline-close-24px.svg</file>
+        <file>images/icons/cross_black_24dp.svg</file>
         <file>images/icons/baseline-done-24px.svg</file>
         <file>images/icons/baseline-error_outline-24px.svg</file>
         <file>projectcredits.html</file>
diff --git a/src/calladapter.cpp b/src/calladapter.cpp
index 57f70c1bb..cd5f72b65 100644
--- a/src/calladapter.cpp
+++ b/src/calladapter.cpp
@@ -643,29 +643,9 @@ CallAdapter::updateCallOverlay(const lrc::api::conversation::Info& convInfo)
 }
 
 void
-CallAdapter::hangupCall(const QString& uri)
+CallAdapter::hangUpCall(const QString& callId)
 {
-    const auto& convInfo = lrcInstance_->getConversationFromPeerUri(uri, accountId_);
-    if (!convInfo.uid.isEmpty()) {
-        auto callModel = lrcInstance_->getAccountInfo(accountId_).callModel.get();
-        if (callModel->hasCall(convInfo.callId)) {
-            /*
-             * Store the last remaining participant of the conference,
-             * so we can switch the smartlist index after termination.
-             */
-            if (!convInfo.confId.isEmpty()) {
-                auto callList = lrcInstance_->getConferenceSubcalls(convInfo.confId);
-                if (callList.size() == 2) {
-                    for (const auto& cId : callList) {
-                        if (cId != convInfo.callId) {
-                            lrcInstance_->pushlastConference(convInfo.confId, cId);
-                        }
-                    }
-                }
-            }
-            callModel->hangUp(convInfo.callId);
-        }
-    }
+    lrcInstance_->getCurrentCallModel()->hangUp(callId);
 }
 
 void
diff --git a/src/calladapter.h b/src/calladapter.h
index 06d9b84aa..1e56d3df8 100644
--- a/src/calladapter.h
+++ b/src/calladapter.h
@@ -56,7 +56,7 @@ public:
     Q_INVOKABLE void sipInputPanelPlayDTMF(const QString& key);
 
     // For Call Overlay
-    Q_INVOKABLE void hangupCall(const QString& uri);
+    Q_INVOKABLE void hangUpCall(const QString& callId);
     Q_INVOKABLE void maximizeParticipant(const QString& uri);
     Q_INVOKABLE void minimizeParticipant(const QString& uri);
     Q_INVOKABLE void hangUpThisCall();
diff --git a/src/calloverlaymodel.cpp b/src/calloverlaymodel.cpp
index beb49d019..a450ac685 100644
--- a/src/calloverlaymodel.cpp
+++ b/src/calloverlaymodel.cpp
@@ -112,6 +112,127 @@ IndexRangeFilterProxyModel::setRange(int min, int max)
     invalidateFilter();
 }
 
+PendingConferenceesListModel::PendingConferenceesListModel(LRCInstance* instance, QObject* parent)
+    : QAbstractListModel(parent)
+    , lrcInstance_(instance)
+{
+    connectSignals();
+    connect(lrcInstance_, &LRCInstance::currentAccountIdChanged, [this]() { connectSignals(); });
+}
+
+int
+PendingConferenceesListModel::rowCount(const QModelIndex& parent) const
+{
+    if (parent.isValid())
+        return 0;
+    return lrcInstance_->getCurrentCallModel()->getPendingConferencees().size();
+}
+
+QVariant
+PendingConferenceesListModel::data(const QModelIndex& index, int role) const
+{
+    using namespace PendingConferences;
+
+    // WARNING: not swarm ready
+    QString pendingConferenceeCallId;
+    QString pendingConferenceeContactUri;
+    ContactModel* contactModel {nullptr};
+    lrc::api::call::Status callStatus;
+    try {
+        auto callModel = lrcInstance_->getCurrentCallModel();
+        auto currentPendingConferenceeInfo = callModel->getPendingConferencees().at(index.row());
+        pendingConferenceeCallId = currentPendingConferenceeInfo.callId;
+        const auto call = callModel->getCall(pendingConferenceeCallId);
+
+        callStatus = call.status;
+        pendingConferenceeContactUri = currentPendingConferenceeInfo.uri;
+        contactModel = lrcInstance_->getCurrentContactModel();
+    } catch (...) {
+        return QVariant(false);
+    }
+
+    // Since we are using image provider right now, image url representation should be unique to
+    // be able to use the image cache, account avatar will only be updated once PictureUid changed
+    switch (role) {
+    case Role::PrimaryName:
+        return QVariant(contactModel->bestNameForContact(pendingConferenceeContactUri));
+    case Role::CallStatus:
+        return QVariant(lrc::api::call::to_string(callStatus));
+    case Role::ContactUri:
+        return QVariant(pendingConferenceeContactUri);
+    case Role::PendingConferenceeCallId:
+        return QVariant(pendingConferenceeCallId);
+    }
+    return QVariant();
+}
+
+QHash<int, QByteArray>
+PendingConferenceesListModel::roleNames() const
+{
+    using namespace PendingConferences;
+    QHash<int, QByteArray> roles;
+#define X(role) roles[role] = #role;
+    PC_ROLES
+#undef X
+    return roles;
+}
+
+void
+PendingConferenceesListModel::connectSignals()
+{
+    beginResetModel();
+
+    disconnect(callsStatusChanged_);
+    disconnect(beginInsertPendingConferencesRows_);
+    disconnect(endInsertPendingConferencesRows_);
+    disconnect(beginRemovePendingConferencesRows_);
+    disconnect(endRemovePendingConferencesRows_);
+
+    callsStatusChanged_ = connect(lrcInstance_->getCurrentCallModel(),
+                                  &NewCallModel::callStatusChanged,
+                                  this,
+                                  [this]() {
+                                      dataChanged(index(0, 0),
+                                                  index(rowCount() - 1),
+                                                  {PendingConferences::Role::CallStatus});
+                                  });
+
+    beginInsertPendingConferencesRows_ = connect(
+        lrcInstance_->getCurrentCallModel(),
+        &NewCallModel::beginInsertPendingConferenceesRows,
+        this,
+        [this](int position, int rows) {
+            beginInsertRows(QModelIndex(), position, position + (rows - 1));
+        },
+        Qt::DirectConnection);
+
+    endInsertPendingConferencesRows_ = connect(
+        lrcInstance_->getCurrentCallModel(),
+        &NewCallModel::endInsertPendingConferenceesRows,
+        this,
+        [this]() { endInsertRows(); },
+        Qt::DirectConnection);
+
+    beginRemovePendingConferencesRows_ = connect(
+        lrcInstance_->getCurrentCallModel(),
+        &NewCallModel::beginRemovePendingConferenceesRows,
+        this,
+        [this](int position, int rows) {
+            beginRemoveRows(QModelIndex(), position, position + (rows - 1));
+        },
+        Qt::DirectConnection);
+
+    endRemovePendingConferencesRows_ = connect(
+        lrcInstance_->getCurrentCallModel(),
+        &NewCallModel::endRemovePendingConferenceesRows,
+        this,
+        [this]() { endRemoveRows(); },
+
+        Qt::DirectConnection);
+
+    endResetModel();
+}
+
 CallOverlayModel::CallOverlayModel(LRCInstance* instance, QObject* parent)
     : QObject(parent)
     , lrcInstance_(instance)
@@ -120,6 +241,7 @@ CallOverlayModel::CallOverlayModel(LRCInstance* instance, QObject* parent)
     , overflowModel_(new IndexRangeFilterProxyModel(secondaryModel_))
     , overflowVisibleModel_(new IndexRangeFilterProxyModel(secondaryModel_))
     , overflowHiddenModel_(new IndexRangeFilterProxyModel(secondaryModel_))
+    , pendingConferenceesModel_(new PendingConferenceesListModel(instance, this))
 {
     connect(this,
             &CallOverlayModel::overflowIndexChanged,
@@ -177,6 +299,12 @@ CallOverlayModel::overflowHiddenModel()
     return QVariant::fromValue(overflowHiddenModel_);
 }
 
+QVariant
+CallOverlayModel::pendingConferenceesModel()
+{
+    return QVariant::fromValue(pendingConferenceesModel_);
+}
+
 void
 CallOverlayModel::clearControls()
 {
diff --git a/src/calloverlaymodel.h b/src/calloverlaymodel.h
index 1776184a9..5aecc90e2 100644
--- a/src/calloverlaymodel.h
+++ b/src/calloverlaymodel.h
@@ -27,6 +27,12 @@
 #include <QSortFilterProxyModel>
 #include <QQuickItem>
 
+#define PC_ROLES \
+    X(PrimaryName) \
+    X(PendingConferenceeCallId) \
+    X(CallStatus) \
+    X(ContactUri)
+
 namespace CallControl {
 Q_NAMESPACE
 enum Role { ItemAction = Qt::UserRole + 1, BadgeCount };
@@ -39,6 +45,17 @@ struct Item
 };
 } // namespace CallControl
 
+namespace PendingConferences {
+Q_NAMESPACE
+enum Role {
+    DummyRole = Qt::UserRole + 1,
+#define X(role) role,
+    PC_ROLES
+#undef X
+};
+Q_ENUM_NS(Role)
+} // namespace PendingConferences
+
 class CallControlListModel : public QAbstractListModel
 {
     Q_OBJECT
@@ -73,6 +90,28 @@ private:
     int max_ {-1};
 };
 
+class PendingConferenceesListModel : public QAbstractListModel
+{
+    Q_OBJECT
+public:
+    PendingConferenceesListModel(LRCInstance* instance, QObject* parent = nullptr);
+
+    int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+    QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+    QHash<int, QByteArray> roleNames() const override;
+
+    void connectSignals();
+
+private:
+    LRCInstance* lrcInstance_ {nullptr};
+
+    QMetaObject::Connection callsStatusChanged_;
+    QMetaObject::Connection beginInsertPendingConferencesRows_;
+    QMetaObject::Connection endInsertPendingConferencesRows_;
+    QMetaObject::Connection beginRemovePendingConferencesRows_;
+    QMetaObject::Connection endRemovePendingConferencesRows_;
+};
+
 class CallOverlayModel : public QObject
 {
     Q_OBJECT
@@ -91,6 +130,7 @@ public:
     Q_INVOKABLE QVariant overflowModel();
     Q_INVOKABLE QVariant overflowVisibleModel();
     Q_INVOKABLE QVariant overflowHiddenModel();
+    Q_INVOKABLE QVariant pendingConferenceesModel();
 
     Q_INVOKABLE void registerFilter(QQuickWindow* object, QQuickItem* item);
     Q_INVOKABLE void unregisterFilter(QQuickWindow* object, QQuickItem* item);
@@ -110,6 +150,7 @@ private:
     IndexRangeFilterProxyModel* overflowModel_;
     IndexRangeFilterProxyModel* overflowVisibleModel_;
     IndexRangeFilterProxyModel* overflowHiddenModel_;
+    PendingConferenceesListModel* pendingConferenceesModel_;
 
     QList<QQuickItem*> watchedItems_;
 };
diff --git a/src/commoncomponents/AvatarImage.qml b/src/commoncomponents/AvatarImage.qml
index 4a40cd5fd..b10a8bb63 100644
--- a/src/commoncomponents/AvatarImage.qml
+++ b/src/commoncomponents/AvatarImage.qml
@@ -23,10 +23,10 @@ import net.jami.Adapters 1.0
 import net.jami.Constants 1.0
 import net.jami.Models 1.0
 
-Item {
+SpinningAnimation {
     id: root
 
-    enum Mode {
+    enum AvatarMode {
         FromAccount = 0,
         FromFile,
         FromContactUri,
@@ -40,22 +40,22 @@ Item {
     property alias sourceSize: rootImage.sourceSize
     property int transitionDuration: 150
     property bool saveToConfig: false
-    property int mode: AvatarImage.Mode.FromAccount
+    property int avatarMode: AvatarImage.AvatarMode.FromAccount
     property string imageProviderIdPrefix: {
-        switch(mode) {
-        case AvatarImage.Mode.FromAccount:
+        switch (avatarMode) {
+        case AvatarImage.AvatarMode.FromAccount:
             return "account_"
-        case AvatarImage.Mode.FromFile:
+        case AvatarImage.AvatarMode.FromFile:
             return "file_"
-        case AvatarImage.Mode.FromContactUri:
+        case AvatarImage.AvatarMode.FromContactUri:
             return "contact_"
-        case AvatarImage.Mode.FromConvUid:
+        case AvatarImage.AvatarMode.FromConvUid:
             return "conversation_"
-        case AvatarImage.Mode.FromTemporaryName:
+        case AvatarImage.AvatarMode.FromTemporaryName:
             return "fallback_"
-        case AvatarImage.Mode.FromBase64:
+        case AvatarImage.AvatarMode.FromBase64:
             return "base64_"
-        case AvatarImage.Mode.Default:
+        case AvatarImage.AvatarMode.Default:
             return "default_"
         default:
             return ""
@@ -63,24 +63,23 @@ Item {
     }
 
     // Full request url example: forceUpdateUrl_xxxxxxx_account_xxxxxxxx
-    property string imageProviderUrl: "image://avatarImage/" + forceUpdateUrl + "_" +
-                                      imageProviderIdPrefix
+    property string imageProviderUrl: "image://avatarImage/" + forceUpdateUrl
+                                      + "_" + imageProviderIdPrefix
     property string imageId: ""
     property string forceUpdateUrl: Date.now()
     property alias presenceStatus: presenceIndicator.status
     property bool showPresenceIndicator: true
     property int unreadMessagesCount: 0
-    property bool enableAnimation: true
-    property bool showSpinningAnimation: false
+    property bool enableFadeAnimation: true
 
     signal imageIsReady
 
     function saveAvatarToConfig() {
-        switch(mode) {
-        case AvatarImage.Mode.FromFile:
+        switch (avatarMode) {
+        case AvatarImage.AvatarMode.FromFile:
             AccountAdapter.setCurrAccAvatar(true, imageId)
             break
-        case AvatarImage.Mode.FromBase64:
+        case AvatarImage.AvatarMode.FromBase64:
             AccountAdapter.setCurrAccAvatar(false, imageId)
             break
         default:
@@ -102,60 +101,38 @@ Item {
     }
 
     function reloadImageSource() {
-        var tempEnableAnimation = enableAnimation
+        var tempEnableAnimation = enableFadeAnimation
         var tempImageSource = rootImage.source
 
-        enableAnimation = false
+        enableFadeAnimation = false
         rootImage.source = ""
 
-        enableAnimation = tempEnableAnimation
+        enableFadeAnimation = tempEnableAnimation
         rootImage.source = tempImageSource
     }
 
     function rootImageOverlayReadyCallback() {
-        if (rootImageOverlay.status === Image.Ready &&
-                (rootImageOverlay.state === "avatarImgFadeIn")) {
-            rootImageOverlay.statusChanged.disconnect(rootImageOverlayReadyCallback)
+        if (rootImageOverlay.status === Image.Ready
+                && (rootImageOverlay.state === "avatarImgFadeIn")) {
+            rootImageOverlay.statusChanged.disconnect(
+                        rootImageOverlayReadyCallback)
 
             rootImageOverlay.state = ''
         }
     }
 
-    Image {
-        id: rootImage
+    Item {
+        id: imageGroup
 
-        anchors.fill: root
+        anchors.centerIn: root
 
-        smooth: true
-        antialiasing: true
-        asynchronous: true
-
-        sourceSize.width: Math.max(24, width)
-        sourceSize.height: Math.max(24, height)
-
-        fillMode: Image.PreserveAspectFit
-
-        onStatusChanged: {
-            if (status === Image.Ready) {
-                if (enableAnimation) {
-                    rootImageOverlay.state = "avatarImgFadeIn"
-                } else {
-                    rootImageOverlay.source = rootImage.source
-                    root.imageIsReady()
-                }
-            }
-        }
-
-        Component.onCompleted: {
-            if (imageId)
-                return source = imageProviderUrl + imageId
-            return source = ""
-        }
+        width: root.width - spinningAnimationWidth
+        height: root.height - spinningAnimationWidth
 
         Image {
-            id: rootImageOverlay
+            id: rootImage
 
-            anchors.fill: rootImage
+            anchors.fill: imageGroup
 
             smooth: true
             antialiasing: true
@@ -166,65 +143,87 @@ Item {
 
             fillMode: Image.PreserveAspectFit
 
-            states: State {
-                name: "avatarImgFadeIn"
-                PropertyChanges {
-                    target: rootImageOverlay
-                    opacity: 0
+            onStatusChanged: {
+                if (status === Image.Ready) {
+                    if (enableFadeAnimation) {
+                        rootImageOverlay.state = "avatarImgFadeIn"
+                    } else {
+                        rootImageOverlay.source = rootImage.source
+                        root.imageIsReady()
+                    }
                 }
             }
 
-            transitions: Transition {
-                NumberAnimation {
-                    properties: "opacity"
-                    easing.type: Easing.InOutQuad
-                    duration: enableAnimation ? 400 : 0
+            Component.onCompleted: {
+                if (imageId)
+                    return source = imageProviderUrl + imageId
+                return source = ""
+            }
+
+            Image {
+                id: rootImageOverlay
+
+                anchors.fill: rootImage
+
+                smooth: true
+                antialiasing: true
+                asynchronous: true
+
+                sourceSize.width: Math.max(24, width)
+                sourceSize.height: Math.max(24, height)
+
+                fillMode: Image.PreserveAspectFit
+
+                states: State {
+                    name: "avatarImgFadeIn"
+                    PropertyChanges {
+                        target: rootImageOverlay
+                        opacity: 0
+                    }
                 }
 
-                onRunningChanged: {
-                    if ((rootImageOverlay.state === "avatarImgFadeIn") && (!running)) {
-                        if (rootImageOverlay.source === rootImage.source) {
-                            rootImageOverlay.state = ''
-                            return
+                transitions: Transition {
+                    NumberAnimation {
+                        properties: "opacity"
+                        easing.type: Easing.InOutQuad
+                        duration: enableFadeAnimation ? 400 : 0
+                    }
+
+                    onRunningChanged: {
+                        if ((rootImageOverlay.state === "avatarImgFadeIn")
+                                && (!running)) {
+                            if (rootImageOverlay.source === rootImage.source) {
+                                rootImageOverlay.state = ''
+                                return
+                            }
+                            rootImageOverlay.statusChanged.connect(
+                                        rootImageOverlayReadyCallback)
+                            rootImageOverlay.source = rootImage.source
                         }
-                        rootImageOverlay.statusChanged.connect(rootImageOverlayReadyCallback)
-                        rootImageOverlay.source = rootImage.source
                     }
                 }
             }
         }
-    }
 
-    PresenceIndicator {
-        id: presenceIndicator
+        PresenceIndicator {
+            id: presenceIndicator
 
-        anchors.right: root.right
-        anchors.rightMargin: -1
-        anchors.bottom: root.bottom
-        anchors.bottomMargin: -1
+            anchors.right: imageGroup.right
+            anchors.rightMargin: -1
+            anchors.bottom: imageGroup.bottom
+            anchors.bottomMargin: -1
 
-        size: root.width * 0.26
+            size: imageGroup.width * 0.26
 
-        visible: showPresenceIndicator
-    }
-
-    SpinningAnimation {
-        id: spinningAnimation
-
-        anchors.horizontalCenter: root.horizontalCenter
-        anchors.verticalCenter: root.verticalCenter
-
-        visible: showSpinningAnimation
-        width: Math.ceil(root.width * 1.05)
-        height: Math.ceil(root.height * 1.05)
-        z: -1
-    }
+            visible: showPresenceIndicator
+        }
 
-    Connections {
-        target: ScreenInfo
+        Connections {
+            target: ScreenInfo
 
-        function onDevicePixelRatioChanged(){
-            reloadImageSource()
+            function onDevicePixelRatioChanged() {
+                reloadImageSource()
+            }
         }
     }
 }
diff --git a/src/commoncomponents/PhotoboothView.qml b/src/commoncomponents/PhotoboothView.qml
index 5f0e35c59..19434d548 100644
--- a/src/commoncomponents/PhotoboothView.qml
+++ b/src/commoncomponents/PhotoboothView.qml
@@ -49,7 +49,7 @@ ColumnLayout {
         photoState = PhotoboothView.PhotoState.Default
         avatarSet = false
         if (useDefaultAvatar)
-            setAvatarImage(AvatarImage.Mode.Default, "")
+            setAvatarImage(AvatarImage.AvatarMode.Default, "")
     }
 
     function startBooth() {
@@ -65,16 +65,16 @@ ColumnLayout {
         } catch(erro){console.log("Exception: " +  erro.message)}
     }
 
-    function setAvatarImage(mode = AvatarImage.Mode.FromAccount,
+    function setAvatarImage(mode = AvatarImage.AvatarMode.FromAccount,
                             imageId = LRCInstance.currentAccountId){
-        if (mode !== AvatarImage.Mode.FromBase64)
-            avatarImg.enableAnimation = true
+        if (mode !== AvatarImage.AvatarMode.FromBase64)
+            avatarImg.enableFadeAnimation = true
         else
-            avatarImg.enableAnimation = false
+            avatarImg.enableFadeAnimation = false
 
-        avatarImg.mode = mode
+        avatarImg.avatarMode = mode
 
-        if (mode === AvatarImage.Mode.Default) {
+        if (mode === AvatarImage.AvatarMode.Default) {
             avatarImg.updateImage(imageId)
             return
         }
@@ -116,7 +116,7 @@ ColumnLayout {
                 return
             }
 
-            setAvatarImage(AvatarImage.Mode.FromFile,
+            setAvatarImage(AvatarImage.AvatarMode.FromFile,
                            UtilsAdapter.getAbsPath(fileName))
         }
     }
@@ -161,7 +161,7 @@ ColumnLayout {
                 }
 
                 onImageIsReady: {
-                    if (mode === AvatarImage.Mode.FromBase64)
+                    if (avatarMode === AvatarImage.AvatarMode.FromBase64)
                         photoState = PhotoboothView.PhotoState.Taken
 
                     if (photoState === PhotoboothView.PhotoState.Taken) {
@@ -268,7 +268,7 @@ ColumnLayout {
                     startBooth()
                     return
                 } else {
-                    setAvatarImage(AvatarImage.Mode.FromBase64,
+                    setAvatarImage(AvatarImage.AvatarMode.FromBase64,
                                    previewWidget.takePhoto(boothWidth))
 
                     avatarSet = true
diff --git a/src/commoncomponents/SpinningAnimation.qml b/src/commoncomponents/SpinningAnimation.qml
index 6147893af..f8061b23f 100644
--- a/src/commoncomponents/SpinningAnimation.qml
+++ b/src/commoncomponents/SpinningAnimation.qml
@@ -1,6 +1,7 @@
 /*
  * Copyright (C) 2021 by Savoir-faire Linux
  * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,8 +26,23 @@ import QtGraphicalEffects 1.12
 Item {
     id: root
 
+    enum SpinningAnimationMode {
+        DISABLED = 0,
+        NORMAL,
+        SYMMETRY
+    }
+
+    property int spinningAnimationMode: SpinningAnimation.SpinningAnimationMode.DISABLED
+    property int spinningAnimationWidth: 5
+    property real outerCutRadius: root.height / 2
+    property int spinningAnimationDuration: 1000
+
     ConicalGradient {
+        id: conicalGradientOne
+
         anchors.fill: parent
+
+        visible: spinningAnimationMode !== SpinningAnimation.SpinningAnimationMode.DISABLED
         angle: 0.0
         gradient: Gradient {
             GradientStop { position: 0.5; color: "transparent" }
@@ -35,17 +51,74 @@ Item {
 
         RotationAnimation on angle {
             loops: Animation.Infinite
-            duration: 1000
+            duration: spinningAnimationDuration
             from: 0
             to: 360
         }
+
+        layer.enabled: true
+        layer.effect: OpacityMask {
+            invert: true
+            maskSource: Item {
+                width: conicalGradientOne.width
+                height: conicalGradientOne.height
+
+                Rectangle {
+                    anchors.fill: parent
+                    anchors.margins: spinningAnimationWidth
+                    radius: outerCutRadius
+                }
+            }
+        }
+    }
+
+    ConicalGradient {
+        id: conicalGradientTwo
+
+        anchors.fill: parent
+
+        visible: spinningAnimationMode === SpinningAnimation.SpinningAnimationMode.SYMMETRY
+        angle: 180.0
+        gradient: Gradient {
+            GradientStop {
+                position: 0.75
+                color: "transparent"
+            }
+            GradientStop {
+                position: 1.0
+                color: "white"
+            }
+        }
+
+        RotationAnimation on angle {
+            loops: Animation.Infinite
+            duration: spinningAnimationDuration
+            from: 180.0
+            to: 540.0
+        }
+
+        layer.enabled: true
+        layer.effect: OpacityMask {
+            invert: true
+            maskSource: Item {
+                width: conicalGradientTwo.width
+                height: conicalGradientTwo.height
+
+                Rectangle {
+                    anchors.fill: parent
+                    anchors.margins: spinningAnimationWidth
+                    radius: outerCutRadius
+                }
+            }
+        }
     }
-    layer.enabled: true
+
+    layer.enabled: spinningAnimationMode !== SpinningAnimation.SpinningAnimationMode.DISABLED
     layer.effect: OpacityMask {
         maskSource: Rectangle {
-            width: root.height
+            width: root.width
             height: root.height
-            radius: root.height / 2
+            radius: outerCutRadius
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml
index 3cb860b18..a3357ba03 100644
--- a/src/constant/JamiTheme.qml
+++ b/src/constant/JamiTheme.qml
@@ -129,6 +129,9 @@ Item {
     // Plugin Preferences View
     property color comboBoxBackgroundColor: darkTheme ? editBackgroundColor : selectedColor
 
+    // ParticipantCallInStatusView
+    property color participantCallInStatusTextColor: whiteColor
+
     // Chatview
     property color jamiLightBlue: darkTheme ? "#003b4e" : Qt.rgba(59, 193, 211, 0.3)
     property color jamiDarkBlue: darkTheme ? "#28b1ed" : "#003b4e"
@@ -191,6 +194,16 @@ Item {
     property real smartListAvatarSize: 52
     property real avatarSizeInCall: 130
     property real callButtonPreferredSize: 50
+    property int participantCallInStatusViewWidth: 225
+    property int participantCallInStatusViewHeight: 300
+    property int participantCallInStatusDelegateHeight: 105
+    property int participantCallInStatusDelegateRadius: 5
+    property real participantCallInStatusOpacity: 0.77
+    property int participantCallInAvatarSize: 75
+    property int participantCallInNameFontSize: 11
+    property int participantCallInStatusFontSize: 9
+    property int participantCallInStatusTextWidthLimit: 100
+    property int participantCallInStatusTextWidth: 68
 
     property real maximumWidthSettingsView: 600
     property real settingsHeaderpreferredHeight: 64
diff --git a/src/mainview/components/ContactPickerItemDelegate.qml b/src/mainview/components/ContactPickerItemDelegate.qml
index d71bd2bcf..6c9a7d32a 100644
--- a/src/mainview/components/ContactPickerItemDelegate.qml
+++ b/src/mainview/components/ContactPickerItemDelegate.qml
@@ -39,7 +39,7 @@ ItemDelegate {
         width: 40
         height: 40
 
-        mode: AvatarImage.Mode.FromContactUri
+        avatarMode: AvatarImage.AvatarMode.FromContactUri
         imageId: URI
     }
 
diff --git a/src/mainview/components/InitialCallPage.qml b/src/mainview/components/InitialCallPage.qml
index 9c22c21e5..1c1484baf 100644
--- a/src/mainview/components/InitialCallPage.qml
+++ b/src/mainview/components/InitialCallPage.qml
@@ -34,7 +34,7 @@ Rectangle {
 
     property bool isIncoming: false
     property bool isAudioOnly: false
-    property var accountConvPair: ["",""]
+    property var accountConvPair: ["", ""]
     property int callStatus: 0
     property string bestName: ""
 
@@ -76,12 +76,12 @@ Rectangle {
             id: contactImg
 
             Layout.alignment: Qt.AlignHCenter
-            Layout.preferredWidth: JamiTheme.avatarSizeInCall
-            Layout.preferredHeight: JamiTheme.avatarSizeInCall
+            Layout.preferredWidth: JamiTheme.avatarSizeInCall + spinningAnimationWidth
+            Layout.preferredHeight: JamiTheme.avatarSizeInCall + spinningAnimationWidth
 
-            mode: AvatarImage.Mode.FromConvUid
+            avatarMode: AvatarImage.AvatarMode.FromConvUid
             showPresenceIndicator: false
-            showSpinningAnimation: true
+            spinningAnimationMode: SpinningAnimation.SpinningAnimationMode.NORMAL
         }
 
         Text {
diff --git a/src/mainview/components/MainOverlay.qml b/src/mainview/components/MainOverlay.qml
index fd9632a81..d47de0998 100644
--- a/src/mainview/components/MainOverlay.qml
+++ b/src/mainview/components/MainOverlay.qml
@@ -44,7 +44,8 @@ Item {
 
     property bool frozen: callActionBar.overflowOpen ||
                           callActionBar.hovered ||
-                          callActionBar.subMenuOpen
+                          callActionBar.subMenuOpen ||
+                          participantCallInStatusView.visible
 
     opacity: 0
 
@@ -184,6 +185,15 @@ Item {
         }
     }
 
+    ParticipantCallInStatusView {
+        id: participantCallInStatusView
+
+        anchors.right: root.right
+        anchors.rightMargin: 10
+        anchors.bottom: __callActionBar.top
+        anchors.bottomMargin: 20
+    }
+
     CallActionBar {
         id: __callActionBar
 
diff --git a/src/mainview/components/OngoingCallPage.qml b/src/mainview/components/OngoingCallPage.qml
index c4cec1c0b..a68f2f42d 100644
--- a/src/mainview/components/OngoingCallPage.qml
+++ b/src/mainview/components/OngoingCallPage.qml
@@ -348,7 +348,7 @@ Rectangle  {
                         Layout.preferredWidth: JamiTheme.avatarSizeInCall
                         Layout.preferredHeight: JamiTheme.avatarSizeInCall
 
-                        mode: AvatarImage.Mode.FromConvUid
+                        avatarMode: AvatarImage.AvatarMode.FromConvUid
                         showPresenceIndicator: false
                     }
 
diff --git a/src/mainview/components/ParticipantCallInStatusDelegate.qml b/src/mainview/components/ParticipantCallInStatusDelegate.qml
new file mode 100644
index 000000000..d73c3991b
--- /dev/null
+++ b/src/mainview/components/ParticipantCallInStatusDelegate.qml
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+import QtQuick.Layouts 1.14
+
+import net.jami.Adapters 1.0
+import net.jami.Models 1.0
+import net.jami.Constants 1.0
+
+import "../../commoncomponents"
+
+SpinningAnimation {
+    id: root
+
+    width: contentRect.width + spinningAnimationWidth
+    height: JamiTheme.participantCallInStatusDelegateHeight
+
+    spinningAnimationMode: SpinningAnimation.SpinningAnimationMode.SYMMETRY
+    outerCutRadius: JamiTheme.participantCallInStatusDelegateRadius
+    spinningAnimationDuration: 5000
+
+    Rectangle {
+        id: contentRect
+
+        anchors.centerIn: root
+
+        width: JamiTheme.participantCallInStatusViewWidth + callStatus.Layout.preferredWidth
+               - JamiTheme.participantCallInStatusTextWidth - spinningAnimationWidth
+        height: JamiTheme.participantCallInStatusDelegateHeight - spinningAnimationWidth
+
+        color: JamiTheme.darkGreyColor
+        opacity: JamiTheme.participantCallInStatusOpacity
+        radius: JamiTheme.participantCallInStatusDelegateRadius
+
+        AvatarImage {
+            id: avatar
+
+            anchors.left: contentRect.left
+            anchors.leftMargin: 10
+            anchors.verticalCenter: contentRect.verticalCenter
+
+            width: JamiTheme.participantCallInAvatarSize
+            height: JamiTheme.participantCallInAvatarSize
+
+            showPresenceIndicator: false
+            avatarMode: AvatarImage.AvatarMode.FromContactUri
+            imageId: ContactUri
+        }
+
+        ColumnLayout {
+            id: infoColumnLayout
+
+            anchors.left: avatar.right
+            anchors.leftMargin: 5
+            anchors.verticalCenter: contentRect.verticalCenter
+
+            implicitHeight: 50
+            implicitWidth: JamiTheme.participantCallInStatusTextWidth
+
+            spacing: 5
+
+            Text {
+                id: name
+
+                Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+                Layout.preferredWidth: JamiTheme.participantCallInStatusTextWidth
+
+                font.weight: Font.ExtraBold
+                font.pointSize: JamiTheme.participantCallInNameFontSize
+                color: JamiTheme.participantCallInStatusTextColor
+                text: PrimaryName
+                elide: Text.ElideRight
+            }
+
+            Text {
+                id: callStatus
+
+                Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+
+                font.weight: Font.Normal
+                font.pointSize: JamiTheme.participantCallInStatusFontSize
+                color: JamiTheme.participantCallInStatusTextColor
+                text: CallStatus + "…"
+                elide: Text.ElideRight
+
+                onWidthChanged: {
+                    if (width > JamiTheme.participantCallInStatusTextWidth
+                            && width < JamiTheme.participantCallInStatusTextWidthLimit)
+                        callStatus.Layout.preferredWidth = width
+                    else if (width >= JamiTheme.participantCallInStatusTextWidthLimit)
+                        callStatus.Layout.preferredWidth
+                                = JamiTheme.participantCallInStatusTextWidthLimit
+                    else
+                        callStatus.Layout.preferredWidth
+                                = JamiTheme.participantCallInStatusTextWidth
+                }
+            }
+        }
+
+        PushButton {
+            id: callCancelButton
+
+            anchors.right: contentRect.right
+            anchors.rightMargin: 10
+            anchors.verticalCenter: contentRect.verticalCenter
+
+            width: 40
+            height: 40
+            // To control the size of the svg
+            preferredSize: 50
+
+            pressedColor: JamiTheme.refuseRed
+            hoveredColor: JamiTheme.refuseRed
+            normalColor: JamiTheme.refuseRedTransparent
+
+            source: "qrc:/images/icons/cross_black_24dp.svg"
+            imageColor: JamiTheme.whiteColor
+
+            toolTipText: JamiStrings.optionCancel
+
+            onClicked: CallAdapter.hangUpCall(PendingConferenceeCallId)
+        }
+    }
+}
diff --git a/src/mainview/components/ParticipantCallInStatusView.qml b/src/mainview/components/ParticipantCallInStatusView.qml
new file mode 100644
index 000000000..81790813d
--- /dev/null
+++ b/src/mainview/components/ParticipantCallInStatusView.qml
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+import QtQuick.Layouts 1.14
+
+import net.jami.Models 1.0
+import net.jami.Adapters 1.0
+import net.jami.Constants 1.0
+
+ListView {
+    id: root
+
+    width: currentItem ? currentItem.width + currentItem.spinningAnimationWidth
+                       : JamiTheme.participantCallInStatusViewWidth
+    height: JamiTheme.participantCallInStatusDelegateHeight
+
+    model: CallOverlayModel.pendingConferenceesModel()
+    delegate: ParticipantCallInStatusDelegate {}
+
+    visible: currentItem ? true : false
+
+    Connections {
+        target: model
+
+        function onRowsInserted() {
+            var preferredHeight = JamiTheme.participantCallInStatusDelegateHeight * model.rowCount()
+            root.height = JamiTheme.participantCallInStatusViewHeight
+                    < preferredHeight ? JamiTheme.participantCallInStatusViewHeight
+                                      : preferredHeight
+        }
+
+        function onRowsRemoved() {
+            var preferredHeight = JamiTheme.participantCallInStatusDelegateHeight * model.rowCount()
+            root.height = JamiTheme.participantCallInStatusViewHeight
+                    < preferredHeight ? JamiTheme.participantCallInStatusViewHeight
+                                      : preferredHeight
+        }
+    }
+
+    clip: true
+    maximumFlickVelocity: 1024
+    ScrollIndicator.vertical: ScrollIndicator {}
+}
diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml
index 7d166ae17..6120491fe 100644
--- a/src/mainview/components/ParticipantOverlay.qml
+++ b/src/mainview/components/ParticipantOverlay.qml
@@ -58,16 +58,16 @@ Item {
             contactImage.visible = false
         else {
             if (avatar) {
-                contactImage.mode = AvatarImage.Mode.FromBase64
+                contactImage.avatarMode = AvatarImage.AvatarMode.FromBase64
                 contactImage.updateImage(avatar)
             } else if (local) {
-                contactImage.mode = AvatarImage.Mode.FromAccount
+                contactImage.avatarMode = AvatarImage.AvatarMode.FromAccount
                 contactImage.updateImage(LRCInstance.currentAccountId)
             } else if (isContact) {
-                contactImage.mode = AvatarImage.Mode.FromContactUri
+                contactImage.avatarMode = AvatarImage.AvatarMode.FromContactUri
                 contactImage.updateImage(uri)
             } else {
-                contactImage.mode = AvatarImage.Mode.FromTemporaryName
+                contactImage.avatarMode = AvatarImage.AvatarMode.FromTemporaryName
                 contactImage.updateImage(uri)
             }
             contactImage.visible = true
@@ -195,7 +195,7 @@ Item {
         fillMode: Image.PreserveAspectFit
         imageId: ""
         visible: false
-        mode: AvatarImage.Mode.Default
+        avatarMode: AvatarImage.AvatarMode.Default
         showPresenceIndicator: false
 
         layer.enabled: true
diff --git a/src/mainview/components/SmartListItemDelegate.qml b/src/mainview/components/SmartListItemDelegate.qml
index 02f449d29..263d9981d 100644
--- a/src/mainview/components/SmartListItemDelegate.qml
+++ b/src/mainview/components/SmartListItemDelegate.qml
@@ -68,7 +68,7 @@ ItemDelegate {
             Layout.preferredWidth: JamiTheme.smartListAvatarSize
             Layout.preferredHeight: JamiTheme.smartListAvatarSize
 
-            mode: AvatarImage.Mode.FromContactUri
+            avatarMode: AvatarImage.AvatarMode.FromContactUri
             showPresenceIndicator: Presence === undefined ? false : Presence
             transitionDuration: 0
         }
diff --git a/src/mainview/components/UserProfile.qml b/src/mainview/components/UserProfile.qml
index ac0690dcc..0e0f6b874 100644
--- a/src/mainview/components/UserProfile.qml
+++ b/src/mainview/components/UserProfile.qml
@@ -66,7 +66,7 @@ BaseDialog {
                 sourceSize.width: preferredImgSize
                 sourceSize.height: preferredImgSize
 
-                mode: AvatarImage.Mode.FromConvUid
+                avatarMode: AvatarImage.AvatarMode.FromConvUid
                 showPresenceIndicator: false
             }
 
diff --git a/src/settingsview/components/ContactItemDelegate.qml b/src/settingsview/components/ContactItemDelegate.qml
index 92a8ce1c7..36faaae89 100644
--- a/src/settingsview/components/ContactItemDelegate.qml
+++ b/src/settingsview/components/ContactItemDelegate.qml
@@ -65,7 +65,7 @@ ItemDelegate {
 
                     anchors.fill: parent
 
-                    mode: AvatarImage.Mode.FromContactUri
+                    avatarMode: AvatarImage.AvatarMode.FromContactUri
                     showPresenceIndicator: false
 
                     fillMode: Image.PreserveAspectCrop
diff --git a/src/wizardview/components/ProfilePage.qml b/src/wizardview/components/ProfilePage.qml
index 79b6c1641..d308c0756 100644
--- a/src/wizardview/components/ProfilePage.qml
+++ b/src/wizardview/components/ProfilePage.qml
@@ -54,7 +54,7 @@ Rectangle {
     color: JamiTheme.backgroundColor
 
     onCreatedAccountIdChanged: {
-        setAvatarWidget.setAvatarImage(AvatarImage.Mode.FromAccount,
+        setAvatarWidget.setAvatarImage(AvatarImage.AvatarMode.FromAccount,
                                        createdAccountId)
     }
 
@@ -126,14 +126,14 @@ Rectangle {
             onTextEdited: {
                 if (!(setAvatarWidget.avatarSet)) {
                     if (text.length === 0) {
-                        setAvatarWidget.setAvatarImage(AvatarImage.Mode.FromAccount,
+                        setAvatarWidget.setAvatarImage(AvatarImage.AvatarMode.FromAccount,
                                                        createdAccountId)
                         return
                     }
 
                     if (text.length == 1 && text.charAt(0) !== lastInitialCharacter) {
                         lastInitialCharacter = text.charAt(0)
-                        setAvatarWidget.setAvatarImage(AvatarImage.Mode.FromTemporaryName,
+                        setAvatarWidget.setAvatarImage(AvatarImage.AvatarMode.FromTemporaryName,
                                                        text)
                     }
                 }
-- 
GitLab