From 6607e0e9cfc7c94eea3d2ab69351e8d0fbf291b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Fri, 24 Jul 2020 15:15:47 -0400
Subject: [PATCH] videocallpage: handle conference infos

Change-Id: I4cfc7c9c525c66f4e483089864bec059c388a1bd
---
 .gitreview                                    |   2 +-
 images/icons/ic_call_end_white_24px.svg       |   2 +-
 qml.qrc                                       |   2 +
 src/calladapter.cpp                           | 194 +++++++++++++++++-
 src/calladapter.h                             |  11 +-
 src/distantrenderer.cpp                       |  39 +++-
 src/distantrenderer.h                         |  11 +
 src/lrcinstance.h                             |   2 +-
 src/mainview/components/CallOverlay.qml       |  49 ++++-
 .../components/CallOverlayButtonGroup.qml     |  14 +-
 src/mainview/components/CallStackView.qml     |  12 +-
 .../components/ParticipantContextMenu.qml     | 127 ++++++++++++
 .../components/ParticipantOverlay.qml         | 169 +++++++++++++++
 src/mainview/components/VideoCallPage.qml     |  15 ++
 14 files changed, 629 insertions(+), 20 deletions(-)
 create mode 100644 src/mainview/components/ParticipantContextMenu.qml
 create mode 100644 src/mainview/components/ParticipantOverlay.qml

diff --git a/.gitreview b/.gitreview
index 08e68f356..382e2644e 100644
--- a/.gitreview
+++ b/.gitreview
@@ -1,6 +1,6 @@
 [gerrit]
 host=gerrit-ring.savoirfairelinux.com
 port=29420
-project=ring-client-window.git
+project=jami-client-qt.git
 defaultremote=origin
 defaultbranch=master
diff --git a/images/icons/ic_call_end_white_24px.svg b/images/icons/ic_call_end_white_24px.svg
index 9c90cb39c..09f87248d 100644
--- a/images/icons/ic_call_end_white_24px.svg
+++ b/images/icons/ic_call_end_white_24px.svg
@@ -1,4 +1,4 @@
-<svg fill="#ffffff" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
     <path d="M0 0h24v24H0z" fill="none"/>
     <path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08c-.18-.17-.29-.42-.29-.7 0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/>
 </svg>
diff --git a/qml.qrc b/qml.qrc
index b8483b66d..19294fa8d 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -67,6 +67,7 @@
         <file>src/mainview/components/ContactSearchBar.qml</file>
         <file>src/mainview/components/VideoCallPage.qml</file>
         <file>src/mainview/components/CallAdvancedOptions.qml</file>
+        <file>src/mainview/components/ParticipantOverlay.qml</file>
         <file>src/mainview/components/ChangeLogScrollView.qml</file>
         <file>src/mainview/components/ProjectCreditsScrollView.qml</file>
         <file>src/mainview/components/AccountComboBoxPopup.qml</file>
@@ -77,6 +78,7 @@
         <file>src/commoncomponents/GeneralMenuItem.qml</file>
         <file>src/mainview/components/ConversationSmartListContextMenu.qml</file>
         <file>src/mainview/components/CallViewContextMenu.qml</file>
+        <file>src/mainview/components/ParticipantContextMenu.qml</file>
         <file>src/commoncomponents/GeneralMenuSeparator.qml</file>
         <file>src/mainview/components/UserProfile.qml</file>
         <file>src/mainview/js/videodevicecontextmenuitemcreation.js</file>
diff --git a/src/calladapter.cpp b/src/calladapter.cpp
index 8cd968231..26ec8c09f 100644
--- a/src/calladapter.cpp
+++ b/src/calladapter.cpp
@@ -37,7 +37,7 @@ CallAdapter::~CallAdapter() {}
 void
 CallAdapter::initQmlObject()
 {
-    connectCallStatusChanged(LRCInstance::getCurrAccId());
+    connectCallModel(LRCInstance::getCurrAccId());
 
     connect(&LRCInstance::behaviorController(),
             &BehaviorController::showIncomingCallView,
@@ -233,12 +233,91 @@ CallAdapter::shouldShowPreview(bool force)
     return shouldShowPreview;
 }
 
+QVariantList
+CallAdapter::getConferencesInfos()
+{
+    QVariantList map;
+    auto convInfo = LRCInstance::getConversationFromConvUid(convUid_, accountId_);
+    if (convInfo.uid.isEmpty())
+        return map;
+    auto callId = convInfo.confId.isEmpty()? convInfo.callId : convInfo.confId;
+    if (!callId.isEmpty()) {
+        try {
+            auto call = LRCInstance::getCurrentCallModel()->getCall(callId);
+            for (const auto& participant: call.participantsInfos) {
+                QJsonObject data;
+                data["x"] = participant["x"].toInt();
+                data["y"] = participant["y"].toInt();
+                data["w"] = participant["w"].toInt();
+                data["h"] = participant["h"].toInt();
+                data["active"] = participant["active"] == "true";
+                auto bestName = participant["uri"];
+                auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId_);
+                data["isLocal"] = false;
+                if (bestName == accInfo.profileInfo.uri) {
+                    bestName = tr("me");
+                    data["isLocal"] = true;
+                } else {
+                    try {
+                        auto &contact = LRCInstance::getCurrentAccountInfo().contactModel->getContact(participant["uri"]);
+                        bestName = Utils::bestNameForContact(contact);
+                    } catch (...) {}
+                }
+                data["bestName"] = bestName;
+
+                map.push_back(QVariant(data));
+            }
+            return map;
+        } catch (...) {}
+    }
+    return map;
+}
+
 void
-CallAdapter::connectCallStatusChanged(const QString &accountId)
+CallAdapter::connectCallModel(const QString &accountId)
 {
     auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
 
     QObject::disconnect(callStatusChangedConnection_);
+    QObject::disconnect(onParticipantsChangedConnection_);
+
+    onParticipantsChangedConnection_ = QObject::connect(
+        accInfo.callModel.get(),
+        &lrc::api::NewCallModel::onParticipantsChanged,
+        [this, accountId](const QString &confId) {
+            auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
+            auto &callModel = accInfo.callModel;
+            auto call = callModel->getCall(confId);
+            auto convInfo = LRCInstance::getConversationFromCallId(confId);
+            if (!convInfo.uid.isEmpty()) {
+                // Convert to QML
+                QVariantList map;
+                for (const auto& participant: call.participantsInfos) {
+                    QJsonObject data;
+                    data["x"] = participant["x"].toInt();
+                    data["y"] = participant["y"].toInt();
+                    data["w"] = participant["w"].toInt();
+                    data["h"] = participant["h"].toInt();
+                    data["uri"] = participant["uri"];
+                    data["active"] = participant["active"] == "true";
+                    auto bestName = participant["uri"];
+                    data["isLocal"] = false;
+                    auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId_);
+                    if (bestName == accInfo.profileInfo.uri) {
+                        bestName = tr("me");
+                        data["isLocal"] = true;
+                    } else {
+                        try {
+                            auto &contact = LRCInstance::getCurrentAccountInfo().contactModel->getContact(participant["uri"]);
+                            bestName = Utils::bestNameForContact(contact);
+                        } catch (...) {}
+                    }
+                    data["bestName"] = bestName;
+                    map.push_back(QVariant(data));
+                }
+                emit updateParticipantsInfos(map, accountId, confId);
+            }
+    });
 
     callStatusChangedConnection_ = QObject::connect(
         accInfo.callModel.get(),
@@ -345,7 +424,7 @@ CallAdapter::updateCallOverlay(const lrc::api::conversation::Info &convInfo)
     bool isAudioOnly = call->isAudioOnly && !isPaused;
     bool isAudioMuted = call->audioMuted && (call->status != lrc::api::call::Status::PAUSED);
     bool isVideoMuted = call->videoMuted && !isPaused && !call->isAudioOnly;
-    bool isRecording = accInfo.callModel->isRecording(convInfo.callId);
+    bool isRecording = isRecordingThisCall();
 
     emit updateOverlay(isPaused,
                        isAudioOnly,
@@ -359,16 +438,16 @@ CallAdapter::updateCallOverlay(const lrc::api::conversation::Info &convInfo)
 }
 
 void
-CallAdapter::hangUpThisCall()
+CallAdapter::hangupCall(const QString& uri)
 {
-    auto convInfo = LRCInstance::getConversationFromConvUid(convUid_, accountId_);
+    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.
-             */
+            * Store the last remaining participant of the conference,
+            * so we can switch the smartlist index after termination.
+            */
             if (!convInfo.confId.isEmpty()) {
                 auto callList = LRCInstance::getAPI().getConferenceSubcalls(convInfo.confId);
                 if (callList.size() == 2) {
@@ -379,6 +458,73 @@ CallAdapter::hangUpThisCall()
                     }
                 }
             }
+
+            callModel->hangUp(convInfo.callId);
+        }
+    }
+}
+
+void
+CallAdapter::maximizeParticipant(const QString& uri, bool isActive)
+{
+    auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
+    auto confId = LRCInstance::getCurrentConversation().confId;
+    QString callId;
+    if (LRCInstance::getCurrentAccountInfo().profileInfo.uri != uri) {
+        auto convInfo = LRCInstance::getConversationFromPeerUri(uri, accountId_);
+        if (!convInfo.uid.isEmpty()) {
+            callId = convInfo.callId;
+        }
+    }
+    try {
+        auto call = callModel->getCall(confId);
+        switch (call.layout) {
+            case lrc::api::call::Layout::GRID:
+                callModel->setActiveParticipant(confId, callId);
+                callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
+                break;
+            case lrc::api::call::Layout::ONE_WITH_SMALL:
+                callModel->setActiveParticipant(confId, callId);
+                callModel->setConferenceLayout(confId,
+                    isActive? lrc::api::call::Layout::ONE : lrc::api::call::Layout::ONE_WITH_SMALL);
+                break;
+            case lrc::api::call::Layout::ONE:
+                callModel->setActiveParticipant(confId, callId);
+                callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
+                break;
+        };
+    } catch (...) {}
+}
+
+void
+CallAdapter::minimizeParticipant()
+{
+    auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
+    auto confId = LRCInstance::getCurrentConversation().confId;
+    try {
+        auto call = callModel->getCall(confId);
+        switch (call.layout) {
+            case lrc::api::call::Layout::GRID:
+                break;
+            case lrc::api::call::Layout::ONE_WITH_SMALL:
+                callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
+                break;
+            case lrc::api::call::Layout::ONE:
+                callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
+                break;
+        };
+    } catch (...) {}
+}
+
+void
+CallAdapter::hangUpThisCall()
+{
+    auto convInfo = LRCInstance::getConversationFromConvUid(convUid_, accountId_);
+    if (!convInfo.uid.isEmpty()) {
+        auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
+        if (!convInfo.confId.isEmpty() && callModel->hasCall(convInfo.confId)) {
+            callModel->hangUp(convInfo.confId);
+        } else if (callModel->hasCall(convInfo.callId)) {
             callModel->hangUp(convInfo.callId);
         }
     }
@@ -393,6 +539,38 @@ CallAdapter::isRecordingThisCall()
     || accInfo.callModel->isRecording(convInfo.callId);
 }
 
+bool
+CallAdapter::isCurrentMaster() const
+{
+    auto convInfo = LRCInstance::getConversationFromConvUid(convUid_, accountId_);
+    if (!convInfo.uid.isEmpty()) {
+        auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
+        try {
+            if (!convInfo.confId.isEmpty() && callModel->hasCall(convInfo.confId)) {
+                return true;
+            } else {
+                auto call = callModel->getCall(convInfo.callId);
+                return call.participantsInfos.size() == 0;
+            }
+        } catch (...) {}
+    }
+    return true;
+}
+
+int
+CallAdapter::getCurrentLayoutType() const
+{
+    auto convInfo = LRCInstance::getConversationFromConvUid(convUid_, accountId_);
+    if (!convInfo.uid.isEmpty()) {
+        auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
+        try {
+            auto call = callModel->getCall(convInfo.confId);
+            return Utils::toUnderlyingValue(call.layout);
+        } catch (...) {}
+    }
+    return -1;
+}
+
 void
 CallAdapter::holdThisCallToggle()
 {
diff --git a/src/calladapter.h b/src/calladapter.h
index b68e08a6e..ce439731b 100644
--- a/src/calladapter.h
+++ b/src/calladapter.h
@@ -24,6 +24,7 @@
 
 #include <QObject>
 #include <QString>
+#include <QVariant>
 
 class CallAdapter : public QmlAdapterBase
 {
@@ -44,17 +45,23 @@ public:
     Q_INVOKABLE void refuseACall(const QString &accountId, const QString &convUid);
     Q_INVOKABLE void acceptACall(const QString &accountId, const QString &convUid);
 
-    Q_INVOKABLE void connectCallStatusChanged(const QString &accountId);
+    Q_INVOKABLE void connectCallModel(const QString &accountId);
 
     /*
      * For Call Overlay
      */
+    Q_INVOKABLE void hangupCall(const QString& uri);
+    Q_INVOKABLE void maximizeParticipant(const QString& uri, bool isActive);
+    Q_INVOKABLE void minimizeParticipant();
     Q_INVOKABLE void hangUpThisCall();
+    Q_INVOKABLE bool isCurrentMaster() const;
+    Q_INVOKABLE int  getCurrentLayoutType() const;
     Q_INVOKABLE void holdThisCallToggle();
     Q_INVOKABLE void muteThisCallToggle();
     Q_INVOKABLE void recordThisCallToggle();
     Q_INVOKABLE void videoPauseThisCallToggle();
     Q_INVOKABLE bool isRecordingThisCall();
+    Q_INVOKABLE QVariantList getConferencesInfos();
 
 signals:
     void showOutgoingCallPage(const QString &accountId, const QString &convUid);
@@ -66,6 +73,7 @@ signals:
     void closePotentialIncomingCallPageWindow(const QString &accountId, const QString &convUid);
     void callStatusChanged(const QString &status, const QString &accountId, const QString &convUid);
     void updateConversationSmartList();
+    void updateParticipantsInfos(const QVariantList& infos, const QString &accountId, const QString &callId);
 
     void incomingCallNeedToSetupMainView(const QString &accountId, const QString &convUid);
     void previewVisibilityNeedToChange(bool visible);
@@ -103,6 +111,7 @@ private:
     QString convUid_;
 
     QMetaObject::Connection callStatusChangedConnection_;
+    QMetaObject::Connection onParticipantsChangedConnection_;
     QMetaObject::Connection closeIncomingCallPageConnection_;
 
     /*
diff --git a/src/distantrenderer.cpp b/src/distantrenderer.cpp
index 023442b34..37b3d4413 100644
--- a/src/distantrenderer.cpp
+++ b/src/distantrenderer.cpp
@@ -51,15 +51,48 @@ DistantRenderer::setRendererId(const QString &id)
     update(QRect(0, 0, width(), height()));
 }
 
+int
+DistantRenderer::getXOffset() const
+{
+    return xOffset_;
+}
+
+int
+DistantRenderer::getYOffset() const
+{
+    return yOffset_;
+}
+
+double
+DistantRenderer::getScaledWidth() const
+{
+    return scaledWidth_;
+}
+
+double
+DistantRenderer::getScaledHeight() const
+{
+    return scaledHeight_;
+}
+
 void
 DistantRenderer::paint(QPainter *painter)
 {
     auto distantImage = LRCInstance::renderer()->getFrame(distantRenderId_);
     if (distantImage) {
         auto scaledDistant = distantImage->scaled(size().toSize(), Qt::KeepAspectRatio);
-        auto xDiff = (width() - scaledDistant.width()) / 2;
-        auto yDiff = (height() - scaledDistant.height()) / 2;
-        painter->drawImage(QRect(xDiff, yDiff, scaledDistant.width(), scaledDistant.height()),
+        auto tempScaledWidth = static_cast<int>(scaledWidth_*1000);
+        auto tempScaledHeight = static_cast<int>(scaledHeight_*1000);
+        auto tempXOffset = xOffset_;
+        auto tempYOffset = yOffset_;
+        scaledWidth_ = static_cast<double>(scaledDistant.width())/static_cast<double>(distantImage->width());
+        scaledHeight_ = static_cast<double>(scaledDistant.height())/static_cast<double>(distantImage->height());
+        xOffset_ = (width() - scaledDistant.width()) / 2;
+        yOffset_ = (height() - scaledDistant.height()) / 2;
+        if (tempXOffset != xOffset_ or tempYOffset != yOffset_ or static_cast<int>(scaledWidth_*1000) != tempScaledWidth or static_cast<int>(scaledHeight_*1000) != tempScaledHeight) {
+            emit offsetChanged();
+        }
+        painter->drawImage(QRect(xOffset_, yOffset_, scaledDistant.width(), scaledDistant.height()),
                            scaledDistant);
     }
 }
\ No newline at end of file
diff --git a/src/distantrenderer.h b/src/distantrenderer.h
index d26f2c2c7..81281e5ef 100644
--- a/src/distantrenderer.h
+++ b/src/distantrenderer.h
@@ -34,6 +34,13 @@ public:
     ~DistantRenderer();
 
     Q_INVOKABLE void setRendererId(const QString &id);
+    Q_INVOKABLE int getXOffset() const;
+    Q_INVOKABLE int getYOffset() const;
+    Q_INVOKABLE double getScaledWidth() const;
+    Q_INVOKABLE double getScaledHeight() const;
+
+signals:
+    void offsetChanged();
 
 private:
     void paint(QPainter *painter);
@@ -42,4 +49,8 @@ private:
      * Unique DistantRenderId for each call.
      */
     QString distantRenderId_;
+    int xOffset_ {0};
+    int yOffset_ {0};
+    double scaledWidth_ {0};
+    double scaledHeight_ {0};
 };
\ No newline at end of file
diff --git a/src/lrcinstance.h b/src/lrcinstance.h
index 190b3839e..262bb1401 100644
--- a/src/lrcinstance.h
+++ b/src/lrcinstance.h
@@ -229,7 +229,7 @@ public:
     {
         return getConversation(
             !accountId.isEmpty() ? accountId : getCurrAccId(),
-            [&](const conversation::Info &conv) -> bool { return callId == conv.callId; },
+            [&](const conversation::Info &conv) -> bool { return callId == conv.callId or callId == conv.confId; },
             filtered);
     }
     static const conversation::Info &
diff --git a/src/mainview/components/CallOverlay.qml b/src/mainview/components/CallOverlay.qml
index 7a6b3f1e1..2f4eac56a 100644
--- a/src/mainview/components/CallOverlay.qml
+++ b/src/mainview/components/CallOverlay.qml
@@ -35,6 +35,9 @@ Rectangle {
 
     signal overlayChatButtonClicked
 
+    property var participantOverlays: []
+    property var participantComponent: Qt.createComponent("ParticipantOverlay.qml")
+
     function setRecording(isRecording) {
         callViewContextMenu.isRecording = isRecording
         recordingRect.visible = isRecording
@@ -52,6 +55,10 @@ Rectangle {
                                                isConferenceCall)
     }
 
+    function updateMaster() {
+        callOverlayButtonGroup.updateMaster()
+    }
+
     function showOnHoldImage(visible) {
         onHoldImage.visible = visible
     }
@@ -60,6 +67,41 @@ Rectangle {
         ContactPickerCreation.closeContactPicker()
     }
 
+    function handleParticipantsInfo(infos) {
+        videoCallOverlay.updateMaster()
+        var isMaster = CallAdapter.isCurrentMaster()
+        for (var p in participantOverlays) {
+            if (participantOverlays[p])
+                participantOverlays[p].destroy()
+        }
+        participantOverlays = []
+        if (infos.length == 0) {
+            previewRenderer.visible = true
+        } else {
+            previewRenderer.visible = false
+            for (var infoVariant in infos) {
+                var hover = participantComponent.createObject(callOverlayRectMouseArea, {
+                    x: distantRenderer.getXOffset() + infos[infoVariant].x * distantRenderer.getScaledWidth(),
+                    y: distantRenderer.getYOffset() + infos[infoVariant].y * distantRenderer.getScaledHeight(),
+                    width: infos[infoVariant].w * distantRenderer.getScaledWidth(),
+                    height: infos[infoVariant].h * distantRenderer.getScaledHeight(),
+                    visible: infos[infoVariant].w != 0 && infos[infoVariant].h != 0
+                })
+                if (!hover) {
+                    console.log("Error when creating the hover")
+                    return
+                }
+                hover.setParticipantName(infos[infoVariant].bestName)
+                hover.active = infos[infoVariant].active;
+                hover.isLocal = infos[infoVariant].isLocal;
+                hover.setMenuVisible(isMaster)
+                hover.uri = infos[infoVariant].uri
+                hover.injectedContextMenu = participantContextMenu
+                participantOverlays.push(hover)
+            }
+        }
+    }
+
     anchors.fill: parent
 
 
@@ -313,10 +355,9 @@ Rectangle {
         id: callOverlayRectMouseArea
 
         anchors.top: callOverlayRect.top
-        anchors.topMargin: 50
 
         width: callOverlayRect.width
-        height: callOverlayRect.height - callOverlayButtonGroup.height - 50
+        height: callOverlayRect.height
 
         hoverEnabled: true
         propagateComposedEvents: true
@@ -370,4 +411,8 @@ Rectangle {
             ContactPickerCreation.openContactPicker()
         }
     }
+
+    ParticipantContextMenu {
+        id: participantContextMenu
+    }
 }
diff --git a/src/mainview/components/CallOverlayButtonGroup.qml b/src/mainview/components/CallOverlayButtonGroup.qml
index cddc0ea85..49511f32c 100644
--- a/src/mainview/components/CallOverlayButtonGroup.qml
+++ b/src/mainview/components/CallOverlayButtonGroup.qml
@@ -34,13 +34,24 @@ Rectangle {
      * since no other methods can make buttons at the layout center.
      */
     property int buttonPreferredSize: 24
+    property var isMaster: true
+    property var isSip: false
 
     signal chatButtonClicked
     signal addToConferenceButtonClicked
 
+    function updateMaster() {
+        root.isMaster = CallAdapter.isCurrentMaster()
+        addToConferenceButton.visible = !root.isSip && root.isMaster
+    }
+
     function setButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted, isRecording, isSIP, isConferenceCall) {
+        root.isMaster = CallAdapter.isCurrentMaster()
+        root.isSip = isSIP
         noVideoButton.visible = !isAudioOnly
-        addToConferenceButton.visible = !isSIP
+        addToConferenceButton.visible = !isSIP && isMaster
+        transferCallButton.visible = isSIP
+        sipInputPanelButton.visible = isSIP
 
         noMicButton.checked = isAudioMuted
         noVideoButton.checked = isVideoMuted
@@ -148,6 +159,7 @@ Rectangle {
 
             Layout.preferredWidth: buttonPreferredSize * 2
             Layout.preferredHeight: buttonPreferredSize * 2
+            visible: !isMaster
 
             backgroundColor: Qt.rgba(0, 0, 0, 0.75)
             onEnterColor: Qt.rgba(0, 0, 0, 0.6)
diff --git a/src/mainview/components/CallStackView.qml b/src/mainview/components/CallStackView.qml
index 27a2b55e8..1f46174b3 100644
--- a/src/mainview/components/CallStackView.qml
+++ b/src/mainview/components/CallStackView.qml
@@ -157,11 +157,17 @@ Rectangle {
         }
 
         function onCallStatusChanged(status, accountId, convUid) {
-            if (responsibleConvUid === convUid
-                    && responsibleAccountId === accountId) {
+            if (responsibleConvUid === convUid && responsibleAccountId === accountId) {
                 outgoingCallPage.callStatusPresentation = status
             }
         }
+
+        function onUpdateParticipantsInfos(infos, accountId, callId) {
+            var responsibleCallId = ClientWrapper.utilsAdaptor.getCallId(responsibleAccountId, responsibleConvUid)
+            if (responsibleCallId === callId) {
+                videoCallPage.handleParticipantsInfo(infos)
+            }
+        }
     }
 
     AudioCallPage {
@@ -198,6 +204,8 @@ Rectangle {
                 videoCallPage.parent = callStackMainView
                 VideoCallFullScreenWindowContainerCreation.closeVideoCallFullScreenWindowContainer()
             }
+
+            videoCallPage.handleParticipantsInfo(CallAdapter.getConferencesInfos())
         }
     }
 
diff --git a/src/mainview/components/ParticipantContextMenu.qml b/src/mainview/components/ParticipantContextMenu.qml
new file mode 100644
index 000000000..9397701cf
--- /dev/null
+++ b/src/mainview/components/ParticipantContextMenu.qml
@@ -0,0 +1,127 @@
+
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * 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 2.14
+import QtQuick.Controls 2.14
+import QtGraphicalEffects 1.12
+import net.jami.Models 1.0
+
+import "../../commoncomponents"
+
+import "../js/videodevicecontextmenuitemcreation.js" as VideoDeviceContextMenuItemCreation
+import "../js/selectscreenwindowcreation.js" as SelectScreenWindowCreation
+
+Menu {
+    id: root
+
+    property int generalMenuSeparatorCount: 0
+    property int commonBorderWidth: 1
+    font.pointSize: JamiTheme.textFontSize + 3
+    property var uri: ""
+    property var maximized: true
+    property var active: true
+
+    function showHangup(show) {
+        if (show) {
+            hangupItem.visible = true
+            hangupItem.height = hangupItem.preferredHeight
+        } else {
+            hangupItem.visible = false
+            hangupItem.height = 0
+        }
+    }
+
+    function showMaximize(show) {
+        if (show) {
+            maximizeItem.visible = true
+            maximizeItem.height = hangupItem.preferredHeight
+        } else {
+            maximizeItem.visible = false
+            maximizeItem.height = 0
+        }
+    }
+
+    function showMinimize(show) {
+        if (show) {
+            minimizeItem.visible = true
+            minimizeItem.height = hangupItem.preferredHeight
+        } else {
+            minimizeItem.visible = false
+            minimizeItem.height = 0
+        }
+    }
+
+    function setHeight(visibleItems) {
+        root.height = hangupItem.preferredHeight * visibleItems;
+    }
+
+    /*
+     * All GeneralMenuItems should remain the same width / height.
+     */
+    GeneralMenuItem {
+        id: hangupItem
+
+        itemName: qsTr("Hangup")
+        iconSource: "qrc:/images/icons/ic_call_end_white_24px.svg"
+        icon.color: "black"
+        leftBorderWidth: commonBorderWidth
+        rightBorderWidth: commonBorderWidth
+
+        onClicked: {
+            CallAdapter.hangupCall(uri)
+            root.close()
+        }
+    }
+    GeneralMenuItem {
+        id: maximizeItem
+
+        itemName: qsTr("Maximize participant")
+        iconSource: "qrc:/images/icons/open_in_full-24px.svg"
+        leftBorderWidth: commonBorderWidth
+        rightBorderWidth: commonBorderWidth
+        visible: !maximized
+
+        onClicked: {
+            CallAdapter.maximizeParticipant(uri, active)
+            root.close()
+        }
+    }
+    GeneralMenuItem {
+        id: minimizeItem
+
+        itemName: qsTr("Minimize participant")
+        iconSource: "qrc:/images/icons/close_fullscreen-24px.svg"
+        leftBorderWidth: commonBorderWidth
+        rightBorderWidth: commonBorderWidth
+        visible: maximized
+
+        onClicked: {
+            CallAdapter.minimizeParticipant()
+            root.close()
+        }
+    }
+
+    background: Rectangle {
+        implicitWidth: hangupItem.preferredWidth
+        implicitHeight: hangupItem.preferredHeight * 3
+
+        border.width: commonBorderWidth
+        border.color: JamiTheme.tabbarBorderColor
+    }
+}
+
diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml
new file mode 100644
index 000000000..fb49a4687
--- /dev/null
+++ b/src/mainview/components/ParticipantOverlay.qml
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * 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 2.14
+import QtQuick.Controls 2.14
+import QtQuick.Layouts 1.14
+import QtQuick.Controls.Universal 2.12
+import QtGraphicalEffects 1.14
+import net.jami.Models 1.0
+
+import "../../commoncomponents"
+
+Rectangle {
+    id: root
+
+    property int buttonPreferredSize: 12
+    property var uri: ""
+    property var active: true
+    property var isLocal: true
+    property var injectedContextMenu: null
+
+    function setParticipantName(name) {
+        participantName.text = name
+    }
+
+    function setMenuVisible(isVisible) {
+        optionsButton.visible = isVisible
+    }
+
+    border.width: 1
+    opacity: 0
+    color: "transparent"
+    z: 1
+
+    MouseArea {
+        id: mouseAreaHover
+        anchors.fill: parent
+        hoverEnabled: true
+        propagateComposedEvents: true
+        acceptedButtons: Qt.LeftButton
+
+        RowLayout {
+            id: bottomLabel
+
+            height: 24
+            width: parent.width
+            anchors.bottom: parent.bottom
+
+            Rectangle {
+                color: "black"
+                opacity: 0.8
+                height: parent.height
+                width: parent.width
+                Layout.fillWidth: true
+                Layout.preferredHeight: parent.height
+
+                Text {
+                    id: participantName
+                    anchors.fill: parent
+                    leftPadding: 8.0
+
+                    TextMetrics {
+                        id: participantMetrics
+                        elide: Text.ElideRight
+                        elideWidth: bottomLabel.width - 8
+                    }
+
+                    text: participantMetrics.elidedText
+
+                    color: "white"
+                    font.pointSize: JamiTheme.textFontSize
+                    horizontalAlignment: Text.AlignLeft
+                    verticalAlignment: Text.AlignVCenter
+                }
+
+                Button {
+                    id: optionsButton
+
+                    anchors.right: parent.right
+                    anchors.verticalCenter: parent.verticalCenter
+
+                    background: Rectangle {
+                        color: "transparent"
+                    }
+
+
+                    icon.color: "white"
+                    icon.height: buttonPreferredSize
+                    icon.width: buttonPreferredSize
+                    icon.source: "qrc:/images/icons/more_vert-24px.svg"
+
+                    onClicked: {
+                        if (!injectedContextMenu) {
+                            console.log("Participant's overlay don't have any injected context menu")
+                            return
+                        }
+                        var mousePos = mapToItem(videoCallPageRect, parent.x, parent.y)
+                        var layout = CallAdapter.getCurrentLayoutType()
+                        var showMaximized = layout !== 2
+                        var showMinimized = !(layout === 0 || (layout === 1 && !active))
+                        injectedContextMenu.showHangup(!root.isLocal)
+                        injectedContextMenu.showMaximize(showMaximized)
+                        injectedContextMenu.showMinimize(showMinimized)
+                        injectedContextMenu.setHeight(
+                            (root.isLocal ? 0 : 1)
+                            + (showMaximized ? 1 : 0)
+                            + (showMinimized ? 1 : 0))
+                        injectedContextMenu.uri = uri
+                        injectedContextMenu.active = active
+                        injectedContextMenu.x = mousePos.x
+                        injectedContextMenu.y = mousePos.y - injectedContextMenu.height
+                        injectedContextMenu.open()
+                    }
+                }
+            }
+        }
+
+        onClicked: {
+            CallAdapter.maximizeParticipant(uri, active)
+        }
+
+        onEntered: {
+            root.state = "entered"
+        }
+
+        onExited: {
+            root.state = "exited"
+        }
+    }
+
+    states: [
+        State {
+            name: "entered"
+            PropertyChanges {
+                target: root
+                opacity: 1
+            }
+        },
+        State {
+            name: "exited"
+            PropertyChanges {
+                target: root
+                opacity: 0
+            }
+        }
+    ]
+
+    transitions: Transition {
+        PropertyAnimation {
+            target: root
+            property: "opacity"
+            duration: 500
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mainview/components/VideoCallPage.qml b/src/mainview/components/VideoCallPage.qml
index 4657a7ed3..047e332d6 100644
--- a/src/mainview/components/VideoCallPage.qml
+++ b/src/mainview/components/VideoCallPage.qml
@@ -42,6 +42,8 @@ Rectangle {
     signal needToShowInFullScreen
 
     function updateUI(accountId, convUid) {
+        videoCallOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos())
+
         bestName = ClientWrapper.utilsAdaptor.getBestName(accountId, convUid)
 
         var id = ClientWrapper.utilsAdaptor.getBestId(accountId, convUid)
@@ -74,6 +76,15 @@ Rectangle {
         videoCallOverlay.closePotentialContactPicker()
     }
 
+    function handleParticipantsInfo(infos) {
+        if (infos.length === 0) {
+            bestName = ClientWrapper.utilsAdaptor.getBestName(accountId, convUid)
+        } else {
+            bestName = ""
+        }
+        videoCallOverlay.handleParticipantsInfo(infos)
+    }
+
     function previewMagneticSnap() {
 
 
@@ -225,6 +236,10 @@ Rectangle {
 
                     width: videoCallPageMainRect.width
                     height: videoCallPageMainRect.height
+
+                    onOffsetChanged: {
+                        videoCallOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos())
+                    }
                 }
 
                 VideoCallPreviewRenderer {
-- 
GitLab