From 824ba581c8dedc33d07e8835acc25f404c650d5d Mon Sep 17 00:00:00 2001
From: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
Date: Mon, 1 Aug 2022 12:32:32 -0300
Subject: [PATCH] callbuttons: create alternate layouts

GitLab: #729

Change-Id: Ice67d8649c1ad2a92eba7c02cebc446eac5ac90e
---
 qml.qrc                                       |   2 +
 resources/icons/ontheside_black_24dp.svg      |   8 +
 resources/icons/onthetop_black_24dp.svg       |   8 +
 src/app/appsettingsmanager.h                  |   1 +
 src/app/calladapter.cpp                       |   1 -
 src/app/constant/JamiStrings.qml              |   2 +
 src/app/mainview/components/CallActionBar.qml |  16 +
 src/app/mainview/components/CallOverlay.qml   |   2 +
 .../mainview/components/OngoingCallPage.qml   |   1 +
 .../mainview/components/ParticipantsLayer.qml | 272 +--------------
 .../ParticipantsLayoutHorizontal.qml          | 321 ++++++++++++++++++
 .../components/ParticipantsLayoutVertical.qml | 293 ++++++++++++++++
 12 files changed, 668 insertions(+), 259 deletions(-)
 create mode 100644 resources/icons/ontheside_black_24dp.svg
 create mode 100644 resources/icons/onthetop_black_24dp.svg
 create mode 100644 src/app/mainview/components/ParticipantsLayoutHorizontal.qml
 create mode 100644 src/app/mainview/components/ParticipantsLayoutVertical.qml

diff --git a/qml.qrc b/qml.qrc
index e7ab3be11..8e731998c 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -136,6 +136,8 @@
         <file>src/app/mainview/components/SmartListItemDelegate.qml</file>
         <file>src/app/mainview/components/BadgeNotifier.qml</file>
         <file>src/app/mainview/components/ParticipantsLayer.qml</file>
+        <file>src/app/mainview/components/ParticipantsLayoutVertical.qml</file>
+        <file>src/app/mainview/components/ParticipantsLayoutHorizontal.qml</file>
         <file>src/app/mainview/components/MainOverlay.qml</file>
         <file>src/app/mainview/components/CallButtonDelegate.qml</file>
         <file>src/app/mainview/components/CallActionBar.qml</file>
diff --git a/resources/icons/ontheside_black_24dp.svg b/resources/icons/ontheside_black_24dp.svg
new file mode 100644
index 000000000..872f97e45
--- /dev/null
+++ b/resources/icons/ontheside_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="M18.3,2H5.8C3.7,2,2,3.7,2,5.8v12.4C2,20.3,3.7,22,5.8,22h12.4c2.1,0,3.8-1.7,3.8-3.7V5.8C22,3.7,20.3,2,18.3,2z M20.6,5.8
+	v2.7h-3.7v-5h1.4C19.6,3.5,20.6,4.5,20.6,5.8z M16.9,10h3.7v4.3h-3.7V10z M3.5,18.2V5.7c0-1.2,1-2.2,2.3-2.2h9.6v17H5.8
+	C4.6,20.5,3.5,19.5,3.5,18.2z M18.3,20.5h-1.4v-4.8h3.7v2.5C20.6,19.5,19.6,20.5,18.3,20.5z"/>
+</svg>
diff --git a/resources/icons/onthetop_black_24dp.svg b/resources/icons/onthetop_black_24dp.svg
new file mode 100644
index 000000000..664e31e0a
--- /dev/null
+++ b/resources/icons/onthetop_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="M18.3,2H5.8C3.7,2,2,3.7,2,5.8v12.4C2,20.3,3.7,22,5.8,22h12.4c2.1,0,3.8-1.7,3.8-3.7V5.8C22,3.7,20.3,2,18.3,2z M20.6,5.8
+	V7h-4.8V3.5h2.5C19.6,3.5,20.6,4.5,20.6,5.8z M10.1,3.5h4.3V7h-4.3V3.5z M5.8,3.5h2.8V7H3.5V5.7C3.5,4.5,4.5,3.5,5.8,3.5z
+	 M18.3,20.5H5.8c-1.2,0-2.3-1-2.3-2.3V8.5h17.1v9.7C20.6,19.5,19.6,20.5,18.3,20.5z"/>
+</svg>
diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h
index 3ecc4a88d..0f13b2a33 100644
--- a/src/app/appsettingsmanager.h
+++ b/src/app/appsettingsmanager.h
@@ -46,6 +46,7 @@ extern const QString defaultDownloadPath;
     X(EnableExperimentalSwarm, false) \
     X(EnableDarkTheme, false) \
     X(BaseZoom, 1.0) \
+    X(ParticipantsSide, false) \
     X(AutoUpdate, true) \
     X(StartMinimized, false) \
     X(ShowChatviewHorizontally, true) \
diff --git a/src/app/calladapter.cpp b/src/app/calladapter.cpp
index 08d8ce0dc..d7ce8a7c8 100644
--- a/src/app/calladapter.cpp
+++ b/src/app/calladapter.cpp
@@ -679,7 +679,6 @@ CallAdapter::sipInputPanelPlayDTMF(const QString& key)
 void
 CallAdapter::updateCallOverlay(const lrc::api::conversation::Info& convInfo)
 {
-    qWarning() << "CallAdapter::updateCallOverlay";
     auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_);
     auto* callModel = accInfo.callModel.get();
 
diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml
index 930c43798..8724cabbf 100644
--- a/src/app/constant/JamiStrings.qml
+++ b/src/app/constant/JamiStrings.qml
@@ -250,6 +250,8 @@ Item {
     property string bothMuted: qsTr("Local and Moderator muted")
     property string moderatorMuted: qsTr("Moderator muted")
     property string notMuted: qsTr("Not muted")
+    property string participantsSide: qsTr("On the side")
+    property string participantsTop: qsTr("On the top")
 
     // LineEditContextMenu
     property string copy: qsTr("Copy")
diff --git a/src/app/mainview/components/CallActionBar.qml b/src/app/mainview/components/CallActionBar.qml
index b1fe1b218..e58221ad5 100644
--- a/src/app/mainview/components/CallActionBar.qml
+++ b/src/app/mainview/components/CallActionBar.qml
@@ -23,6 +23,7 @@ import QtQuick.Layouts
 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 "../../commoncomponents"
 
@@ -163,6 +164,16 @@ Control {
                         if (!isGrid)
                             CallAdapter.showGridConferenceLayout()
                         break
+                  case JamiStrings.participantsSide:
+                        var onTheSide = UtilsAdapter.getAppValue(Settings.ParticipantsSide)
+                        UtilsAdapter.setAppValue(Settings.ParticipantsSide, !onTheSide)
+                        participantsSide = !onTheSide
+                        break
+                  case JamiStrings.participantsTop:
+                        var onTheSide = UtilsAdapter.getAppValue(Settings.ParticipantsSide)
+                        UtilsAdapter.setAppValue(Settings.ParticipantsSide, !onTheSide)
+                        participantsSide = !onTheSide
+                        break
                 }
             }
             onTriggered: {
@@ -172,6 +183,11 @@ Control {
                                     "ActiveSetting": layoutManager.isCallFullscreen})
                 if (isConference) {
                     layoutModel.append({})
+                    var onTheSide = UtilsAdapter.getAppValue(Settings.ParticipantsSide)
+                    layoutModel.append({"Name": onTheSide ? JamiStrings.participantsSide : JamiStrings.participantsTop,
+                                        "IconSource": onTheSide ? JamiResources.ontheside_black_24dp_svg : JamiResources.onthetop_black_24dp_svg,
+                                        "ActiveSetting": true})
+                    layoutModel.append({})
                     layoutModel.append({"Name": JamiStrings.mosaic,
                                         "IconSource": JamiResources.mosaic_black_24dp_svg,
                                         "ActiveSetting": isGrid})
diff --git a/src/app/mainview/components/CallOverlay.qml b/src/app/mainview/components/CallOverlay.qml
index 2a9beb8f2..bb78bbe32 100644
--- a/src/app/mainview/components/CallOverlay.qml
+++ b/src/app/mainview/components/CallOverlay.qml
@@ -23,6 +23,7 @@ import QtQuick
 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 "../js/contactpickercreation.js" as ContactPickerCreation
 import "../js/selectscreenwindowcreation.js" as SelectScreenWindowCreation
@@ -44,6 +45,7 @@ Item {
     property bool isModerator
     property bool isConference
     property bool isGrid
+    property bool participantsSide: UtilsAdapter.getAppValue(Settings.ParticipantsSide)
     property bool localHandRaised
     property bool sharingActive: AvAdapter.isSharing()
     property string callId: ""
diff --git a/src/app/mainview/components/OngoingCallPage.qml b/src/app/mainview/components/OngoingCallPage.qml
index 79e55c502..3674ddc6f 100644
--- a/src/app/mainview/components/OngoingCallPage.qml
+++ b/src/app/mainview/components/OngoingCallPage.qml
@@ -177,6 +177,7 @@ Rectangle {
                 anchors.centerIn: parent
                 anchors.margins: 3
                 visible: participantsLayer.count !== 0
+                participantsSide: callOverlay.participantsSide
 
                 onCountChanged: {
                     callOverlay.isConference = participantsLayer.count > 0
diff --git a/src/app/mainview/components/ParticipantsLayer.qml b/src/app/mainview/components/ParticipantsLayer.qml
index 5881c7f64..ceac7cfc2 100644
--- a/src/app/mainview/components/ParticipantsLayer.qml
+++ b/src/app/mainview/components/ParticipantsLayer.qml
@@ -25,12 +25,15 @@ import QtQuick.Controls 2.15
 import net.jami.Adapters 1.1
 import net.jami.Models 1.1
 import net.jami.Constants 1.1
+import net.jami.Enums 1.1
+import "../../commoncomponents"
 
 Item {
     id: root
 
-    property int count: commonParticipants.count + activeParticipants.count
+    property int count: 0
     property bool inLine: CallParticipantsModel.conferenceLayout === CallParticipantsModel.ONE_WITH_SMALL
+    property bool participantsSide
 
     Component {
         id: callVideoMedia
@@ -70,265 +73,18 @@ Item {
         }
     }
 
-    SplitView {
+    ParticipantsLayoutVertical {
         anchors.fill: parent
+        participantComponent: callVideoMedia
+        visible: !participantsSide
 
-        orientation: Qt.Vertical
-        handle: Rectangle {
-            implicitWidth: root.width
-            implicitHeight: 11
-            color: "transparent"
-            Rectangle {
-                anchors.centerIn: parent
-                height: 1
-                width: parent.implicitWidth - 40
-                color: JamiTheme.darkGreyColor
-            }
-
-            Rectangle {
-                width: 45
-                anchors.centerIn: parent
-                height: 1
-                color: "black"
-            }
-
-            ColumnLayout {
-                anchors.centerIn: parent
-                height: 11
-                width: 45
-                Rectangle {
-                    Layout.fillWidth: true
-                    Layout.leftMargin: 10
-                    Layout.rightMargin: 10
-                    height: 2
-                    color: JamiTheme.darkGreyColor
-                }
-                Rectangle {
-                    Layout.fillWidth: true
-                    Layout.leftMargin: 10
-                    Layout.rightMargin: 10
-                    height: 2
-                    color: JamiTheme.darkGreyColor
-                }
-            }
-        }
-
-        Rectangle {
-            id: genericParticipantsRect
-
-            TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton }
-
-            SplitView.preferredHeight: (parent.height / 4)
-            SplitView.minimumHeight: parent.height / 6
-            SplitView.maximumHeight: inLine? parent.height / 2 : parent.height
-
-            visible: inLine || CallParticipantsModel.conferenceLayout === CallParticipantsModel.GRID
-            color: "transparent"
-
-            property int lowLimit: 0
-            property int topLimit: commonParticipants.count
-            property int currentPos: 0
-            property int showable: {
-                if (!inLine)
-                    return commonParticipants.count
-                if (commonParticipantsFlow.componentWidth === 0)
-                    return 1
-                var placeableElements = Math.floor((width * 0.9)/commonParticipantsFlow.componentWidth)
-                if (commonParticipants.count - placeableElements < currentPos)
-                    currentPos = Math.max(commonParticipants.count - placeableElements, 0)
-                return Math.max(1, placeableElements)
-            }
-
-            RowLayout {
-                anchors.fill: parent
-
-                RoundButton {
-                    Layout.alignment: Qt.AlignVCenter
-                    width : 30
-                    height : 30
-                    radius: 10
-                    text: "<"
-                    visible: genericParticipantsRect.currentPos > 0
-                             && activeParticipantsFlow.visible
-                    onClicked: {
-                        if (genericParticipantsRect.currentPos > 0)
-                            genericParticipantsRect.currentPos--
-                    }
-                    background: Rectangle {
-                        anchors.fill: parent
-                        color: JamiTheme.lightGrey_
-                        radius: JamiTheme.primaryRadius
-                    }
-                }
-
-                Item {
-                    id: centerItem
-                    Layout.fillHeight: true
-                    Layout.fillWidth: true
-                    Layout.margins: 4
-
-                    // GENERIC
-                    Flow {
-                        id: commonParticipantsFlow
-                        anchors.fill: parent
-
-                        spacing: 4
-                        property int columns: {
-                            if (inLine)
-                                return commonParticipants.count
-                            var ratio = Math.floor(root.width / root.height)
-                            // If ratio is 2 we can have 2 times more elements on each columns
-                            var wantedCol = Math.max(1, Math.round(Math.sqrt(commonParticipants.count) * ratio))
-                            var cols =  Math.min(commonParticipants.count, wantedCol)
-                            // Optimize with the rows (eg 7 with ratio 2 should have 4 and 3 items, not 6 and 1)
-                            var rows = Math.max(1, Math.ceil(commonParticipants.count/cols))
-                            return Math.min(Math.ceil(commonParticipants.count / rows), cols)
-                        }
-                        property int rows: Math.max(1, Math.ceil(commonParticipants.count/columns))
-                        property int componentWidth: {
-                            var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.columns
-                            var w = Math.floor((commonParticipantsFlow.width - totalSpacing)/ commonParticipantsFlow.columns)
-                            if (inLine) {
-                                w = Math.max(w, height)
-                                w = Math.min(w, height * 4 / 3) // Avoid too wide elements
-                            }
-                            return w
-                        }
-
-                        Item {
-                            height: parent.height
-                            width: {
-                                if (!inLine)
-                                    return 0
-                                var showed = Math.min(genericParticipantsRect.showable, commonParticipantsFlow.columns)
-                                return Math.max(0, Math.ceil((centerItem.width - commonParticipantsFlow.componentWidth * showed) / 2))
-                            }
-                        }
-
-                        Repeater {
-                            id: commonParticipants
-
-                            model: GenericParticipantsFilterModel
-                            delegate: Loader {
-                                sourceComponent: callVideoMedia
-                                active: root.visible
-                                asynchronous: true
-                                visible: {
-                                    if (status !== Loader.Ready)
-                                        return false
-                                    if (inLine)
-                                        return index >= genericParticipantsRect.currentPos
-                                                && index < genericParticipantsRect.currentPos + genericParticipantsRect.showable
-                                    return true
-                                }
-                                width: commonParticipantsFlow.componentWidth + leftMargin_
-                                height: {
-                                    if (inLine || commonParticipantsFlow.rows === 1)
-                                        return genericParticipantsRect.height
-                                    var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.rows
-                                    return Math.floor((genericParticipantsRect.height - totalSpacing)/ commonParticipantsFlow.rows)
-                                }
-
-                                property int leftMargin_: {
-                                    if (inLine || commonParticipantsFlow.rows === 1)
-                                        return 0
-                                    var lastParticipants = (commonParticipants.count % commonParticipantsFlow.columns)
-                                    if (lastParticipants !== 0 && index === commonParticipants.count - lastParticipants) {
-                                        var compW = commonParticipantsFlow.componentWidth + commonParticipantsFlow.spacing
-                                        var lastLineW = lastParticipants * compW
-                                        return Math.floor((commonParticipantsFlow.width - lastLineW) / 2)
-                                    }
-                                    return 0
-                                }
-
-                                property string uri_: Uri
-                                property string deviceId_: Device
-                                property string bestName_: BestName
-                                property string avatar_: Avatar ? Avatar : ""
-                                property string sinkId_: SinkId ? SinkId : ""
-                                property bool isLocal_: IsLocal
-                                property bool active_: Active
-                                property bool videoMuted_: VideoMuted
-                                property bool isContact_: IsContact
-                                property bool isModerator_: IsModerator
-                                property bool audioLocalMuted_: AudioLocalMuted
-                                property bool audioModeratorMuted_: AudioModeratorMuted
-                                property bool isHandRaised_: HandRaised
-                            }
-                        }
-                    }
-                }
-
-                RoundButton {
-                    Layout.alignment: Qt.AlignVCenter
-                    width : 30
-                    height : 30
-                    radius: 10
-                    text: ">"
-                    visible: genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos
-                             && activeParticipantsFlow.visible
-                    onClicked: {
-                        if (genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos)
-                            genericParticipantsRect.currentPos++
-                    }
-                    background: Rectangle {
-                        anchors.fill: parent
-                        color: JamiTheme.lightGrey_
-                        radius: JamiTheme.primaryRadius
-                    }
-                }
-            }
-        }
-
-        // ACTIVE
-        Flow {
-            id: activeParticipantsFlow
-
-            TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton }
-
-            SplitView.minimumHeight: parent.height / 4
-            SplitView.maximumHeight: parent.height
-            SplitView.fillHeight: true
-
-            spacing: 8
-            property int columns: Math.max(1, Math.ceil(Math.sqrt(activeParticipants.count)))
-            property int rows: Math.max(1, Math.ceil(activeParticipants.count/columns))
-            property int columnsSpacing: 5 * (columns - 1)
-            property int rowsSpacing: 5 * (rows - 1)
-
-            visible: inLine || CallParticipantsModel.conferenceLayout === CallParticipantsModel.ONE
-
-            Repeater {
-                id: activeParticipants
-                anchors.fill: parent
-                anchors.centerIn: parent
-
-                model: ActiveParticipantsFilterModel
-                delegate: Loader {
-                    active: root.visible
-                    asynchronous: true
-                    sourceComponent: callVideoMedia
-                    visible: status == Loader.Ready
-
-                    width: Math.ceil(activeParticipantsFlow.width / activeParticipantsFlow.columns) - activeParticipantsFlow.columnsSpacing
-                    height: Math.ceil(activeParticipantsFlow.height / activeParticipantsFlow.rows) - activeParticipantsFlow.rowsSpacing
+        onLayoutCountChanged: root.count = layoutCount
+    }
 
-                    property string uri_: Uri
-                    property string bestName_: BestName
-                    property string avatar_: Avatar ? Avatar : ""
-                    property string sinkId_: SinkId ? SinkId : ""
-                    property string deviceId_: Device
-                    property int leftMargin_: 0
-                    property bool isLocal_: IsLocal
-                    property bool active_: Active
-                    property bool videoMuted_: VideoMuted
-                    property bool isContact_: IsContact
-                    property bool isModerator_: IsModerator
-                    property bool audioLocalMuted_: AudioLocalMuted
-                    property bool audioModeratorMuted_: AudioModeratorMuted
-                    property bool isHandRaised_: HandRaised
-                }
-            }
-        }
+    ParticipantsLayoutHorizontal {
+        anchors.fill: parent
+        participantComponent: callVideoMedia
+        visible: participantsSide
+        onLayoutCountChanged: root.count = layoutCount
     }
 }
diff --git a/src/app/mainview/components/ParticipantsLayoutHorizontal.qml b/src/app/mainview/components/ParticipantsLayoutHorizontal.qml
new file mode 100644
index 000000000..e0fc08a0d
--- /dev/null
+++ b/src/app/mainview/components/ParticipantsLayoutHorizontal.qml
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2020-2022 Savoir-faire Linux Inc.
+ * Authors: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *          Aline Gondim Santos <aline.gondimsantos@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.Layouts 1.15
+import QtQuick.Controls 2.15
+
+import net.jami.Adapters 1.1
+import net.jami.Models 1.1
+import net.jami.Constants 1.1
+import net.jami.Enums 1.1
+
+SplitView {
+    id: root
+
+    property int layoutCount: commonParticipants.count + activeParticipants.count
+    property var participantComponent
+
+    orientation: Qt.Horizontal
+    handle: Rectangle {
+        implicitHeight: root.height
+        implicitWidth: 11
+        color: "transparent"
+        Rectangle {
+            anchors.centerIn: parent
+            width: 1
+            height: parent.implicitHeight - 40
+            color: JamiTheme.darkGreyColor
+        }
+
+        Rectangle {
+            height: 45
+            anchors.centerIn: parent
+            width: 1
+            color: "black"
+        }
+
+        RowLayout {
+            anchors.centerIn: parent
+            height: 45
+            width: 11
+            Rectangle {
+                Layout.fillHeight: true
+                Layout.topMargin: 10
+                Layout.bottomMargin: 10
+                width: 2
+                color: JamiTheme.darkGreyColor
+            }
+            Rectangle {
+                Layout.fillHeight: true
+                Layout.topMargin: 10
+                Layout.bottomMargin: 10
+                width: 2
+                color: JamiTheme.darkGreyColor
+            }
+        }
+    }
+
+    // ACTIVE
+    Flow {
+        id: activeParticipantsFlow
+
+        TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton }
+
+        SplitView.minimumWidth: parent.width / 4
+        SplitView.maximumWidth: parent.width
+        SplitView.fillWidth: true
+
+        spacing: 8
+        property int columns: Math.max(1, Math.ceil(Math.sqrt(activeParticipants.count)))
+        property int rows: Math.max(1, Math.ceil(activeParticipants.count/columns))
+        property int columnsSpacing: 5 * (columns - 1)
+        property int rowsSpacing: 5 * (rows - 1)
+
+        visible: inLine || CallParticipantsModel.conferenceLayout === CallParticipantsModel.ONE
+
+        Repeater {
+            id: activeParticipants
+            anchors.fill: parent
+            anchors.centerIn: parent
+
+            model: ActiveParticipantsFilterModel
+            delegate: Loader {
+                active: root.visible
+                asynchronous: true
+                sourceComponent: callVideoMedia
+                visible: status == Loader.Ready
+
+                width: Math.ceil(activeParticipantsFlow.width / activeParticipantsFlow.columns) - activeParticipantsFlow.columnsSpacing
+                height: Math.ceil(activeParticipantsFlow.height / activeParticipantsFlow.rows) - activeParticipantsFlow.rowsSpacing
+
+                property string uri_: Uri
+                property string bestName_: BestName
+                property string avatar_: Avatar ? Avatar : ""
+                property string sinkId_: SinkId ? SinkId : ""
+                property string deviceId_: Device
+                property int leftMargin_: 0
+                property bool isLocal_: IsLocal
+                property bool active_: Active
+                property bool videoMuted_: VideoMuted
+                property bool isContact_: IsContact
+                property bool isModerator_: IsModerator
+                property bool audioLocalMuted_: AudioLocalMuted
+                property bool audioModeratorMuted_: AudioModeratorMuted
+                property bool isHandRaised_: HandRaised
+            }
+        }
+    }
+
+    Rectangle {
+        id: genericParticipantsRect
+
+        TapHandler { acceptedButtons: Qt.TopButton | Qt.BottomButton }
+
+        SplitView.preferredWidth: (parent.width / 4)
+        SplitView.minimumWidth: parent.width / 6
+        SplitView.maximumWidth: inLine? parent.width / 2 : parent.width
+
+        visible: inLine || CallParticipantsModel.conferenceLayout === CallParticipantsModel.GRID
+        color: "transparent"
+
+        property int lowLimit: 0
+        property int topLimit: commonParticipants.count
+        property int currentPos: 0
+        property int showable: {
+            if (!inLine)
+                return commonParticipants.count
+            if (commonParticipantsFlow.componentHeight === 0)
+                return 1
+            var placeableElements = Math.floor((height * 0.9)/commonParticipantsFlow.componentHeight)
+            if (commonParticipants.count - placeableElements < currentPos)
+                currentPos = Math.max(commonParticipants.count - placeableElements, 0)
+            return Math.max(1, placeableElements)
+        }
+
+        ColumnLayout {
+            anchors.fill: parent
+            width: parent.width
+
+            RowLayout {
+                Layout.alignment: Qt.AlignHCenter
+                width: parent.width
+                height: 30
+                Layout.bottomMargin: 16
+                Layout.topMargin: 16
+                spacing: 8
+                visible: (genericParticipantsRect.currentPos > 0 && activeParticipantsFlow.visible) ||
+                         (genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos && activeParticipantsFlow.visible)
+
+                RoundButton {
+                    width : 30
+                    height : 30
+                    radius: 10
+                    text: "^"
+                    visible: genericParticipantsRect.currentPos > 0
+                                && activeParticipantsFlow.visible
+                    onClicked: {
+                        if (genericParticipantsRect.currentPos > 0)
+                            genericParticipantsRect.currentPos--
+                    }
+                    background: Rectangle {
+                        anchors.fill: parent
+                        color: JamiTheme.lightGrey_
+                        radius: JamiTheme.primaryRadius
+                    }
+                }
+
+                RoundButton {
+                    width : 30
+                    height : 30
+                    radius: 10
+                    text: "v"
+                    visible: genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos
+                                && activeParticipantsFlow.visible
+                    onClicked: {
+                        if (genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos)
+                            genericParticipantsRect.currentPos++
+                    }
+                    background: Rectangle {
+                        anchors.fill: parent
+                        color: JamiTheme.lightGrey_
+                        radius: JamiTheme.primaryRadius
+                    }
+                }
+            }
+
+            Item {
+                id: centerItem
+                Layout.fillHeight: true
+                Layout.fillWidth: true
+                Layout.margins: 4
+
+                // GENERIC
+                Flow {
+                    id: commonParticipantsFlow
+                    anchors.fill: parent
+
+                    spacing: 4
+                    property int columns: {
+                        if (inLine)
+                            return 1
+                        var ratio = Math.floor(root.width / root.height)
+                        // If ratio is 2 we can have 2 times more elements on each columns
+                        var wantedCol = Math.max(1, Math.round(Math.sqrt(commonParticipants.count) * ratio))
+                        var cols =  Math.min(commonParticipants.count, wantedCol)
+                        // Optimize with the rows (eg 7 with ratio 2 should have 4 and 3 items, not 6 and 1)
+                        var rows = Math.max(1, Math.ceil(commonParticipants.count/cols))
+                        return Math.min(Math.ceil(commonParticipants.count / rows), cols)
+                    }
+                    property int rows: {
+                        if (inLine)
+                            return commonParticipants.count
+                        Math.max(1, Math.ceil(commonParticipants.count/columns))
+                    }
+                    property int componentHeight: {
+                        var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.rows
+                        var h = Math.floor((commonParticipantsFlow.height - totalSpacing)/ commonParticipantsFlow.rows)
+                        if (inLine) {
+                            h = Math.max(width, h)
+                            h = Math.min(width, h * 4 / 3) // Avoid too high elements
+                        }
+                        return h
+                    }
+                    property int componentWidth: {
+                        var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.columns
+                        var w = Math.floor((commonParticipantsFlow.width - totalSpacing)/ commonParticipantsFlow.columns)
+                        if (inLine) {
+                            w = commonParticipantsFlow.width
+                        }
+                        return w
+                    }
+
+                    Item {
+                        width: parent.width
+                        height: {
+                            if (!inLine)
+                                return 0
+	                        var showed = Math.min(genericParticipantsRect.showable, commonParticipantsFlow.rows)
+	                        return Math.max(0, Math.ceil((centerItem.height - commonParticipantsFlow.componentHeight * showed) / 2))
+                        }
+                    }
+
+                    Repeater {
+                        id: commonParticipants
+
+                        model: GenericParticipantsFilterModel
+                        delegate: Loader {
+                            sourceComponent: callVideoMedia
+                            active: root.visible
+                            asynchronous: true
+                            visible: {
+                                if (status !== Loader.Ready)
+                                    return false
+                                if (inLine)
+                                    return index >= genericParticipantsRect.currentPos
+                                            && index < genericParticipantsRect.currentPos + genericParticipantsRect.showable
+                                return true
+                            }
+                            width: commonParticipantsFlow.componentWidth + leftMargin_
+                            height: {
+                                if (inLine || commonParticipantsFlow.columns === 1)
+                                    return commonParticipantsFlow.componentHeight
+                                var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.rows
+                                return Math.floor((genericParticipantsRect.height - totalSpacing) / commonParticipantsFlow.rows)
+                            }
+
+                            property int leftMargin_: {
+                                if (inLine || commonParticipantsFlow.rows === 1)
+                                    return 0
+                                var lastParticipants = (commonParticipants.count % commonParticipantsFlow.columns)
+                                if (lastParticipants !== 0 && index === commonParticipants.count - lastParticipants) {
+                                    var compW = commonParticipantsFlow.componentWidth + commonParticipantsFlow.spacing
+                                    var lastLineW = lastParticipants * compW
+                                    return Math.floor((commonParticipantsFlow.width - lastLineW) / 2)
+                                }
+                                return 0
+                            }
+
+                            property string uri_: Uri
+                            property string deviceId_: Device
+                            property string bestName_: BestName
+                            property string avatar_: Avatar ? Avatar : ""
+                            property string sinkId_: SinkId ? SinkId : ""
+                            property bool isLocal_: IsLocal
+                            property bool active_: Active
+                            property bool videoMuted_: VideoMuted
+                            property bool isContact_: IsContact
+                            property bool isModerator_: IsModerator
+                            property bool audioLocalMuted_: AudioLocalMuted
+                            property bool audioModeratorMuted_: AudioModeratorMuted
+                            property bool isHandRaised_: HandRaised
+                        }
+                    }
+                }
+            }
+
+            Item {
+                Layout.alignment: Qt.AlignHCenter
+                width: parent.width
+                height : 30
+            }
+        }
+    }
+}
diff --git a/src/app/mainview/components/ParticipantsLayoutVertical.qml b/src/app/mainview/components/ParticipantsLayoutVertical.qml
new file mode 100644
index 000000000..6989a879b
--- /dev/null
+++ b/src/app/mainview/components/ParticipantsLayoutVertical.qml
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2020-2022 Savoir-faire Linux Inc.
+ * Authors: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *          Aline Gondim Santos <aline.gondimsantos@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.Layouts 1.15
+import QtQuick.Controls 2.15
+
+import net.jami.Adapters 1.1
+import net.jami.Models 1.1
+import net.jami.Constants 1.1
+import net.jami.Enums 1.1
+
+SplitView {
+    id: root
+
+    property int layoutCount: commonParticipants.count + activeParticipants.count
+    property var participantComponent
+
+    orientation: Qt.Vertical
+    handle: Rectangle {
+        implicitWidth: root.width
+        implicitHeight: 11
+        color: "transparent"
+        Rectangle {
+            anchors.centerIn: parent
+            height: 1
+            width: parent.implicitWidth - 40
+            color: JamiTheme.darkGreyColor
+        }
+
+        Rectangle {
+            width: 45
+            anchors.centerIn: parent
+            height: 1
+            color: "black"
+        }
+
+        ColumnLayout {
+            anchors.centerIn: parent
+            height: 11
+            width: 45
+            Rectangle {
+                Layout.fillWidth: true
+                Layout.leftMargin: 10
+                Layout.rightMargin: 10
+                height: 2
+                color: JamiTheme.darkGreyColor
+            }
+            Rectangle {
+                Layout.fillWidth: true
+                Layout.leftMargin: 10
+                Layout.rightMargin: 10
+                height: 2
+                color: JamiTheme.darkGreyColor
+            }
+        }
+    }
+
+    Rectangle {
+        id: genericParticipantsRect
+
+        TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton }
+
+        SplitView.preferredHeight: (parent.height / 4)
+        SplitView.minimumHeight: parent.height / 6
+        SplitView.maximumHeight: inLine? parent.height / 2 : parent.height
+
+        visible: inLine || CallParticipantsModel.conferenceLayout === CallParticipantsModel.GRID
+        color: "transparent"
+
+        property int lowLimit: 0
+        property int topLimit: commonParticipants.count
+        property int currentPos: 0
+        property int showable: {
+            if (!inLine)
+                return commonParticipants.count
+            if (commonParticipantsFlow.componentWidth === 0)
+                return 1
+            var placeableElements = Math.floor((width * 0.9)/commonParticipantsFlow.componentWidth)
+            if (commonParticipants.count - placeableElements < currentPos)
+                currentPos = Math.max(commonParticipants.count - placeableElements, 0)
+            return Math.max(1, placeableElements)
+        }
+
+        RowLayout {
+            anchors.fill: parent
+
+            RoundButton {
+                Layout.alignment: Qt.AlignVCenter
+                width : 30
+                height : 30
+                radius: 10
+                text: "<"
+                visible: genericParticipantsRect.currentPos > 0
+                            && activeParticipantsFlow.visible
+                onClicked: {
+                    if (genericParticipantsRect.currentPos > 0)
+                        genericParticipantsRect.currentPos--
+                }
+                background: Rectangle {
+                    anchors.fill: parent
+                    color: JamiTheme.lightGrey_
+                    radius: JamiTheme.primaryRadius
+                }
+            }
+
+            Item {
+                id: centerItem
+                Layout.fillHeight: true
+                Layout.fillWidth: true
+                Layout.margins: 4
+
+                // GENERIC
+                Flow {
+                    id: commonParticipantsFlow
+                    anchors.fill: parent
+
+                    spacing: 4
+                    property int columns: {
+                        if (inLine)
+                            return commonParticipants.count
+                        var ratio = Math.floor(root.width / root.height)
+                        // If ratio is 2 we can have 2 times more elements on each columns
+                        var wantedCol = Math.max(1, Math.round(Math.sqrt(commonParticipants.count) * ratio))
+                        var cols =  Math.min(commonParticipants.count, wantedCol)
+                        // Optimize with the rows (eg 7 with ratio 2 should have 4 and 3 items, not 6 and 1)
+                        var rows = Math.max(1, Math.ceil(commonParticipants.count/cols))
+                        return Math.min(Math.ceil(commonParticipants.count / rows), cols)
+                    }
+                    property int rows: Math.max(1, Math.ceil(commonParticipants.count/columns))
+                    property int componentWidth: {
+                        var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.columns
+                        var w = Math.floor((commonParticipantsFlow.width - totalSpacing)/ commonParticipantsFlow.columns)
+                        if (inLine) {
+                            w = Math.max(w, height)
+                            w = Math.min(w, height * 4 / 3) // Avoid too wide elements
+                        }
+                        return w
+                    }
+
+                    Item {
+                        height: parent.height
+                        width: {
+                            if (!inLine)
+                                return 0
+                            var showed = Math.min(genericParticipantsRect.showable, commonParticipantsFlow.columns)
+                            return Math.max(0, Math.ceil((centerItem.width - commonParticipantsFlow.componentWidth * showed) / 2))
+                        }
+                    }
+
+                    Repeater {
+                        id: commonParticipants
+
+                        model: GenericParticipantsFilterModel
+                        delegate: Loader {
+                            sourceComponent: callVideoMedia
+                            active: root.visible
+                            asynchronous: true
+                            visible: {
+                                if (status !== Loader.Ready)
+                                    return false
+                                if (inLine)
+                                    return index >= genericParticipantsRect.currentPos
+                                            && index < genericParticipantsRect.currentPos + genericParticipantsRect.showable
+                                return true
+                            }
+                            width: commonParticipantsFlow.componentWidth + leftMargin_
+                            height: {
+                                if (inLine || commonParticipantsFlow.rows === 1)
+                                    return genericParticipantsRect.height
+                                var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.rows
+                                return Math.floor((genericParticipantsRect.height - totalSpacing)/ commonParticipantsFlow.rows)
+                            }
+
+                            property int leftMargin_: {
+                                if (inLine || commonParticipantsFlow.rows === 1)
+                                    return 0
+                                var lastParticipants = (commonParticipants.count % commonParticipantsFlow.columns)
+                                if (lastParticipants !== 0 && index === commonParticipants.count - lastParticipants) {
+                                    var compW = commonParticipantsFlow.componentWidth + commonParticipantsFlow.spacing
+                                    var lastLineW = lastParticipants * compW
+                                    return Math.floor((commonParticipantsFlow.width - lastLineW) / 2)
+                                }
+                                return 0
+                            }
+
+                            property string uri_: Uri
+                            property string deviceId_: Device
+                            property string bestName_: BestName
+                            property string avatar_: Avatar ? Avatar : ""
+                            property string sinkId_: SinkId ? SinkId : ""
+                            property bool isLocal_: IsLocal
+                            property bool active_: Active
+                            property bool videoMuted_: VideoMuted
+                            property bool isContact_: IsContact
+                            property bool isModerator_: IsModerator
+                            property bool audioLocalMuted_: AudioLocalMuted
+                            property bool audioModeratorMuted_: AudioModeratorMuted
+                            property bool isHandRaised_: HandRaised
+                        }
+                    }
+                }
+            }
+
+            RoundButton {
+                Layout.alignment: Qt.AlignVCenter
+                width : 30
+                height : 30
+                radius: 10
+                text: ">"
+                visible: genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos
+                            && activeParticipantsFlow.visible
+                onClicked: {
+                    if (genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos)
+                        genericParticipantsRect.currentPos++
+                }
+                background: Rectangle {
+                    anchors.fill: parent
+                    color: JamiTheme.lightGrey_
+                    radius: JamiTheme.primaryRadius
+                }
+            }
+        }
+    }
+
+    // ACTIVE
+    Flow {
+        id: activeParticipantsFlow
+
+        TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton }
+
+        SplitView.minimumHeight: parent.height / 4
+        SplitView.maximumHeight: parent.height
+        SplitView.fillHeight: true
+
+        spacing: 8
+        property int columns: Math.max(1, Math.ceil(Math.sqrt(activeParticipants.count)))
+        property int rows: Math.max(1, Math.ceil(activeParticipants.count/columns))
+        property int columnsSpacing: 5 * (columns - 1)
+        property int rowsSpacing: 5 * (rows - 1)
+
+        visible: inLine || CallParticipantsModel.conferenceLayout === CallParticipantsModel.ONE
+
+        Repeater {
+            id: activeParticipants
+            anchors.fill: parent
+            anchors.centerIn: parent
+
+            model: ActiveParticipantsFilterModel
+            delegate: Loader {
+                active: root.visible
+                asynchronous: true
+                sourceComponent: callVideoMedia
+                visible: status == Loader.Ready
+
+                width: Math.ceil(activeParticipantsFlow.width / activeParticipantsFlow.columns) - activeParticipantsFlow.columnsSpacing
+                height: Math.ceil(activeParticipantsFlow.height / activeParticipantsFlow.rows) - activeParticipantsFlow.rowsSpacing
+
+                property string uri_: Uri
+                property string bestName_: BestName
+                property string avatar_: Avatar ? Avatar : ""
+                property string sinkId_: SinkId ? SinkId : ""
+                property string deviceId_: Device
+                property int leftMargin_: 0
+                property bool isLocal_: IsLocal
+                property bool active_: Active
+                property bool videoMuted_: VideoMuted
+                property bool isContact_: IsContact
+                property bool isModerator_: IsModerator
+                property bool audioLocalMuted_: AudioLocalMuted
+                property bool audioModeratorMuted_: AudioModeratorMuted
+                property bool isHandRaised_: HandRaised
+            }
+        }
+    }
+}
-- 
GitLab