From 4a581d0a1a147072080ac62341d1c3e4490c2680 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Fri, 11 Feb 2022 11:39:16 -0500
Subject: [PATCH] swarm: pass members to swarm's creation

This is the first version of the members list for swarm creation.
The user is able to select 1:1 conversation, search new users and
merge swarm members in a new swarm.

Change-Id: Ic9ac1e9324a46f70ad5d285df890a01ca459f3fa
GitLab: #670
---
 src/constant/JamiStrings.qml                  |   5 +-
 src/conversationlistmodel.cpp                 |   4 +-
 src/conversationlistmodel.h                   |   4 +
 src/conversationsadapter.cpp                  |   9 +-
 src/conversationsadapter.h                    |   1 +
 src/mainview/MainView.qml                     |  14 ++-
 .../components/ConversationListView.qml       |   5 +-
 src/mainview/components/NewSwarmPage.qml      |   5 +-
 src/mainview/components/SidePanel.qml         | 112 ++++++++++++++++--
 .../components/SmartListItemDelegate.qml      |  20 +++-
 10 files changed, 151 insertions(+), 28 deletions(-)

diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml
index bb1a7917e..32a2589ef 100644
--- a/src/constant/JamiStrings.qml
+++ b/src/constant/JamiStrings.qml
@@ -443,7 +443,7 @@ Item {
     property string logsViewCopy: qsTr("Copy")
     property string logsViewReport: qsTr("Report Bug")
     property string logsViewClear: qsTr("Clear")
-    property string logsViewCancel: qsTr("Cancel")
+    property string cancel: qsTr("Cancel")
     property string logsViewCopied: qsTr("Copied to clipboard!")
     property string logsViewDisplay: qsTr("Receive Logs")
 
@@ -621,7 +621,8 @@ Item {
     property string editDescription: qsTr("Edit description")
 
     // NewSwarmPage
-
+    property string youCanAdd8: qsTr("You can add 8 people in the swarm")
+    property string youCanAddMore: qsTr("You can add %1 more people in the swarm")
     property string createTheSwarm: qsTr("Create the swarm")
     property string goToConversation: qsTr("Go to conversation")
     property string promoteAdministrator: qsTr("Promote to administrator")
diff --git a/src/conversationlistmodel.cpp b/src/conversationlistmodel.cpp
index 791716a30..389729380 100644
--- a/src/conversationlistmodel.cpp
+++ b/src/conversationlistmodel.cpp
@@ -117,7 +117,9 @@ ConversationListProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& s
     bool match {false};
 
     // banned contacts require exact match
-    if (index.data(Role::IsBanned).toBool()) {
+    if (ignored_.contains(index.data(Role::UID).toString())) {
+        match = true;
+    } else if (index.data(Role::IsBanned).toBool()) {
         if (!rx.isValid()) {
             Q_FOREACH (const auto& filter, toFilter) {
                 auto matchResult = rx.match(filter);
diff --git a/src/conversationlistmodel.h b/src/conversationlistmodel.h
index b67621168..16b0a2a1f 100644
--- a/src/conversationlistmodel.h
+++ b/src/conversationlistmodel.h
@@ -49,9 +49,13 @@ public:
     bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
 
     Q_INVOKABLE void setFilterRequests(bool filterRequests);
+    Q_INVOKABLE void ignoreFiltering(const QStringList& highlighted) {
+        ignored_ = highlighted;
+    }
 
 private:
     // This flag can be toggled when switching tabs to show the current account's
     // conversation invites.
     bool filterRequests_ {false};
+    QStringList ignored_ {};
 };
diff --git a/src/conversationsadapter.cpp b/src/conversationsadapter.cpp
index 509578a2e..d1dcc4e81 100644
--- a/src/conversationsadapter.cpp
+++ b/src/conversationsadapter.cpp
@@ -384,6 +384,12 @@ ConversationsAdapter::setFilter(const QString& filterString)
     Q_EMIT textFilterChanged(filterString);
 }
 
+void
+ConversationsAdapter::ignoreFiltering(const QVariant& hightlighted)
+{
+    convModel_->ignoreFiltering(hightlighted.toStringList());
+}
+
 QVariantMap
 ConversationsAdapter::getConvInfoMap(const QString& convId)
 {
@@ -392,9 +398,9 @@ ConversationsAdapter::getConvInfoMap(const QString& convId)
         return {};
     QString peerUri {};
     QString bestId {};
+    const auto& accountInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
     if (convInfo.isCoreDialog()) {
         try {
-            const auto& accountInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
             peerUri = accountInfo.conversationModel->peersForConversation(convId).at(0);
             bestId = accountInfo.contactModel->bestIdForContact(peerUri);
         } catch (...) {
@@ -425,6 +431,7 @@ ConversationsAdapter::getConvInfoMap(const QString& convId)
             {"bestId", bestId},
             {"title", lrcInstance_->getCurrentConversationModel()->title(convId)},
             {"uri", peerUri},
+            {"uris", accountInfo.conversationModel->peersForConversation(convId)},
             {"isSwarm", convInfo.isSwarm()},
             {"isRequest", convInfo.isRequest},
             {"needsSyncing", convInfo.needsSyncing},
diff --git a/src/conversationsadapter.h b/src/conversationsadapter.h
index 15189542e..de30708c3 100644
--- a/src/conversationsadapter.h
+++ b/src/conversationsadapter.h
@@ -53,6 +53,7 @@ public:
                                  const QString& avatar,
                                  const VectorString& participants);
     Q_INVOKABLE void setFilter(const QString& filterString);
+    Q_INVOKABLE void ignoreFiltering(const QVariant& hightlighted);
     Q_INVOKABLE QVariantMap getConvInfoMap(const QString& convId);
     Q_INVOKABLE void restartConversation(const QString& convId);
     Q_INVOKABLE void updateConversationTitle(const QString& convId, const QString& newTitle);
diff --git a/src/mainview/MainView.qml b/src/mainview/MainView.qml
index 9a4a7aaba..0f8008ab9 100644
--- a/src/mainview/MainView.qml
+++ b/src/mainview/MainView.qml
@@ -369,7 +369,12 @@ Rectangle {
         }
 
         onCreateSwarmClicked: {
-            pushNewSwarmPage()
+            if (newSwarmPage.visible) {
+                backToMainView()
+                mainViewSidePanel.showSwarmListView(false)
+            } else {
+                pushNewSwarmPage()
+            }
         }
     }
 
@@ -417,7 +422,12 @@ Rectangle {
         objectName: "newSwarmPage"
         visible: false
 
-        onCreateSwarmClicked: {
+        onVisibleChanged: {
+            mainViewSidePanel.showSwarmListView(newSwarmPage.visible)
+        }
+
+        onCreateSwarmClicked: function(title, description, avatar) {
+            ConversationsAdapter.createSwarm(title, description, avatar, mainViewSidePanel.highlightedMembers)
             backToMainView()
         }
     }
diff --git a/src/mainview/components/ConversationListView.qml b/src/mainview/components/ConversationListView.qml
index 106f4b87f..e1f523ab9 100644
--- a/src/mainview/components/ConversationListView.qml
+++ b/src/mainview/components/ConversationListView.qml
@@ -39,10 +39,7 @@ JamiListView {
 
     // highlight selection
     // down and hover states are done within the delegate
-    highlight: Rectangle {
-        width: ListView.view ? ListView.view.width : 0
-        color: JamiTheme.selectedColor
-    }
+
     highlightMoveDuration: 60
 
     headerPositioning: ListView.OverlayHeader
diff --git a/src/mainview/components/NewSwarmPage.qml b/src/mainview/components/NewSwarmPage.qml
index 109d9f33b..79dfdd5e1 100644
--- a/src/mainview/components/NewSwarmPage.qml
+++ b/src/mainview/components/NewSwarmPage.qml
@@ -32,7 +32,7 @@ Rectangle {
 
     color: JamiTheme.chatviewBgColor
 
-    signal createSwarmClicked
+    signal createSwarmClicked(string title, string description, string avatar)
 
     ColumnLayout {
         id: mainLayout
@@ -83,8 +83,7 @@ Rectangle {
             text: JamiStrings.createTheSwarm
 
             onClicked: {
-                ConversationsAdapter.createSwarm(title.text, description.text, "", [])
-                createSwarmClicked()
+                createSwarmClicked(title.text, description.text, "")
             }
         }
     }
diff --git a/src/mainview/components/SidePanel.qml b/src/mainview/components/SidePanel.qml
index 894bb9f60..db4160977 100644
--- a/src/mainview/components/SidePanel.qml
+++ b/src/mainview/components/SidePanel.qml
@@ -18,6 +18,7 @@
  */
 
 import QtQuick
+import QtQuick.Controls
 import QtQuick.Layouts
 
 import net.jami.Models 1.1
@@ -27,7 +28,7 @@ import net.jami.Constants 1.1
 import "../../commoncomponents"
 
 Rectangle {
-    id: sidePanelRect
+    id: root
 
     color: JamiTheme.backgroundColor
 
@@ -58,15 +59,39 @@ Rectangle {
         sidePanelTabBar.selectTab(tabIndex)
     }
 
+    property var highlighted: []
+    property var highlightedMembers: []
+
+    function refreshHighlighted() {
+        var result = []
+        for (var idx in highlighted) {
+            var convId = highlighted[idx]
+            var item = ConversationsAdapter.getConvInfoMap(convId)
+            for (var idx in item.uris) {
+                var uri = item.uris[idx]
+                if (!result.indexOf(uri) != -1 && uri != CurrentAccount.uri) {
+                    result.push(uri)
+                }
+            }
+        }
+        highlightedMembers = result
+        ConversationsAdapter.ignoreFiltering(root.highlighted)
+    }
+
+    function showSwarmListView(v) {
+        smartListLayout.visible = !v
+        swarmMemberSearchList.visible = v
+    }
+
     RowLayout {
         id: startBar
 
         height: 40
-        anchors.top: sidePanelRect.top
+        anchors.top: root.top
         anchors.topMargin: 10
-        anchors.left: sidePanelRect.left
+        anchors.left: root.left
         anchors.leftMargin: 15
-        anchors.right: sidePanelRect.right
+        anchors.right: root.right
         anchors.rightMargin: 15
 
         ContactSearchBar {
@@ -79,6 +104,7 @@ Rectangle {
                 // not calling positionViewAtBeginning will cause
                 // sort animation visual bugs
                 conversationListView.positionViewAtBeginning()
+                ConversationsAdapter.ignoreFiltering(root.highlighted)
                 ConversationsAdapter.setFilter(text)
             }
 
@@ -103,12 +129,10 @@ Rectangle {
 
             preferredSize: startBar.height
 
-            source: JamiResources.create_swarm_svg
-            toolTipText: JamiStrings.startASwarm
+            source: smartListLayout.visible ? JamiResources.create_swarm_svg : JamiResources.round_close_24dp_svg
+            toolTipText: smartListLayout.visible ? JamiStrings.startASwarm : JamiStrings.cancel
 
-            onClicked: {
-                createSwarmClicked()
-            }
+            onClicked: createSwarmClicked()
         }
     }
 
@@ -116,10 +140,10 @@ Rectangle {
         id: sidePanelTabBar
 
         visible: ConversationsAdapter.pendingRequestCount &&
-                 !contactSearchBar.textContent
+                 !contactSearchBar.textContent && smartListLayout.visible
         anchors.top: startBar.bottom
         anchors.topMargin: visible ? 10 : 0
-        width: sidePanelRect.width
+        width: root.width
         height: visible ? 42 : 0
         contentHeight: visible ? 42 : 0
     }
@@ -127,7 +151,7 @@ Rectangle {
     Rectangle {
         id: searchStatusRect
 
-        visible: searchStatusText.text !== ""
+        visible: searchStatusText.text !== "" && smartListLayout.visible
 
         anchors.top: sidePanelTabBar.bottom
         anchors.topMargin: visible ? 10 : 0
@@ -214,4 +238,68 @@ Rectangle {
             headerVisible: searchResultsListView.visible
         }
     }
+
+    ColumnLayout {
+        id: swarmMemberSearchList
+
+        visible: false
+
+        width: parent.width
+        anchors.top: searchStatusRect.bottom
+        anchors.topMargin: (sidePanelTabBar.visible ||
+                            searchStatusRect.visible) ? 0 : 12
+        anchors.bottom: parent.bottom
+
+        spacing: 4
+
+        Label {
+            font.bold: true
+            font.pointSize: JamiTheme.contactEventPointSize
+
+            Layout.margins: 16
+            Layout.maximumHeight: 24
+
+            text: {
+                if (highlightedMembers.length === 0)
+                    return JamiStrings.youCanAdd8
+                return JamiStrings.youCanAddMore.arg(8 - Math.min(highlightedMembers.length, 8))
+            }
+            color: JamiTheme.textColor
+        }
+
+        JamiListView {
+            id: swarmCurrentConversationList
+
+            Layout.preferredWidth: parent.width
+            Layout.fillHeight: true
+
+            model: ConversationListModel
+            delegate: SmartListItemDelegate {
+                interactive: false
+
+                onVisibleChanged: {
+                    if (!visible) {
+                        highlighted = false
+                        root.refreshHighlighted()
+                    }
+                }
+
+                onHighlightedChanged: function onHighlightedChanged() {
+                    var currentHighlighted = root.highlighted
+                    if (highlighted) {
+                        root.highlighted.push(convId)
+                    } else {
+                        root.highlighted = Array.from(root.highlighted).filter(r => r !== convId)
+                    }
+                    root.refreshHighlighted()
+                    // We can't have more than 8 participants yet.
+                    if (root.highlightedMembers.length > 8) {
+                        highlighted = false
+                        root.refreshHighlighted()
+                    }
+                }
+            }
+            currentIndex: model.currentFilteredRow
+        }
+    }
 }
diff --git a/src/mainview/components/SmartListItemDelegate.qml b/src/mainview/components/SmartListItemDelegate.qml
index dcf6a7754..da66c81d1 100644
--- a/src/mainview/components/SmartListItemDelegate.qml
+++ b/src/mainview/components/SmartListItemDelegate.qml
@@ -37,6 +37,7 @@ ItemDelegate {
     property string convId: ""
 
     highlighted: ListView.isCurrentItem
+    property bool interactive: true
 
     onVisibleChanged: {
         if (visible)
@@ -174,7 +175,7 @@ ItemDelegate {
 
     background: Rectangle {
         color: {
-            if (root.pressed)
+            if (root.pressed || root.highlighted)
                 return Qt.darker(JamiTheme.selectedColor, 1.1)
             else if (root.hovered)
                 return Qt.darker(JamiTheme.selectedColor, 1.05)
@@ -183,8 +184,16 @@ ItemDelegate {
         }
     }
 
-    onClicked: ListView.view.model.select(index)
+    onClicked: {
+        if (!interactive) {
+            highlighted = !highlighted
+            return;
+        }
+        ListView.view.model.select(index)
+    }
     onDoubleClicked: {
+        if (!interactive)
+            return;
         ListView.view.model.select(index)
         if (LRCInstance.currentAccountType === Profile.Type.SIP || !CurrentAccount.videoEnabled_Video)
             CallAdapter.placeAudioOnlyCall()
@@ -194,10 +203,15 @@ ItemDelegate {
             }
         }
     }
-    onPressAndHold: ListView.view.openContextMenuAt(pressX, pressY, root)
+    onPressAndHold: {
+        if (!interactive)
+            return;
+        ListView.view.openContextMenuAt(pressX, pressY, root)
+    }
 
     MouseArea {
         anchors.fill: parent
+        enabled: interactive
         acceptedButtons: Qt.RightButton
         onClicked: function (mouse) {
             root.ListView.view.openContextMenuAt(mouse.x, mouse.y, root)
-- 
GitLab