From 4788e963a65f0f0b6743be04145f5d3fb2781368 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Fri, 28 Jan 2022 16:54:15 -0500
Subject: [PATCH] swarm: add context menu for members

In the members list, a right click allow the user to access some
actions such as:

+ Perform a video or audio call with a member
+ Open a 1:1 conversation with this member
+ Block this contact
+ If allowed, kick a member from the conversation

In the future, other actions can be added, such as promote a user
to administrator.

GitLab: #340
Change-Id: I3824ad4efa8faf89479e99c93b98d3dd9781582d
---
 resources/icons/block_black_24dp.svg          | 14 +++++----
 resources/icons/gotoconversation.svg          | 13 +++++++++
 resources/icons/kick_member.svg               | 14 +++++++++
 src/contactadapter.cpp                        |  7 +++++
 src/contactadapter.h                          |  4 ++-
 src/conversationsadapter.cpp                  | 19 ++++++++++++
 src/conversationsadapter.h                    |  3 ++
 src/mainview/components/SidePanel.qml         |  6 ++++
 src/mainview/components/SwarmDetailsPanel.qml |  2 ++
 .../SwarmParticipantContextMenu.qml           | 29 +++++++++++++------
 src/utils.cpp                                 |  7 ++---
 11 files changed, 98 insertions(+), 20 deletions(-)
 create mode 100644 resources/icons/gotoconversation.svg
 create mode 100644 resources/icons/kick_member.svg

diff --git a/resources/icons/block_black_24dp.svg b/resources/icons/block_black_24dp.svg
index c24b8807d..3d3b8512e 100644
--- a/resources/icons/block_black_24dp.svg
+++ b/resources/icons/block_black_24dp.svg
@@ -2,10 +2,12 @@
 <!-- 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="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z M4.1,8.4c1.4-3,4.5-5.1,7.9-5.1c2.1,0,4.1,0.7,5.6,2.1l-2,2
-	c-0.3-1.8-1.7-3.1-3.6-3.1C10,4.4,8.4,6,8.4,8c0,1.7,1.1,3,2.6,3.5c-2.7,0.5-4.9,2.9-5.5,6l-0.1,0.1C3.2,15,2.7,11.4,4.1,8.4z
-	 M12.7,10.3c-0.2,0.1-0.5,0.1-0.7,0.1c-1.4,0-2.4-1-2.4-2.4s1-2.4,2.4-2.4c1.4,0,2.4,1,2.4,2.4c0,0.3,0,0.5-0.1,0.7L12.7,10.3z
-	 M9.8,13.2l-2.3,2.3C8.1,14.5,8.9,13.7,9.8,13.2z M6.6,18.8c0-0.2,0-0.4,0-0.6l5.6-5.6c2.8,0.2,5,2.8,5.1,6.1
-	C15.9,20,14,20.7,12,20.7C10,20.7,8.2,20,6.6,18.8z M18.5,17.7c-0.5-3.1-2.5-5.5-5.1-6.1l0.4-0.4c0.5-0.3,1-0.7,1.3-1.3l3.5-3.5
-	c2.2,2.6,2.7,6.2,1.3,9.2C19.5,16.4,19.1,17.1,18.5,17.7z"/>
+<g>
+	<path d="M16.9,11.5c-2.8,0-5.1,2.3-5.1,5.1s2.3,5.1,5.1,5.1s5.1-2.3,5.1-5.1S19.7,11.5,16.9,11.5z M16.9,12.7
+		c0.8,0,1.6,0.3,2.2,0.7l-5.6,5c-0.3-0.6-0.5-1.2-0.5-1.9C13.1,14.5,14.8,12.7,16.9,12.7z M16.9,20.5c-1,0-2-0.4-2.7-1.1l5.7-5.1
+		c0.5,0.6,0.8,1.4,0.8,2.3C20.8,18.7,19.1,20.5,16.9,20.5z"/>
+	<path d="M14.5,13.1l0.7-0.4c-1-0.7-2.1-1.2-3.3-1.4c1.9-0.6,3.2-2.3,3.2-4.4c0.1-2.6-2-4.6-4.5-4.6C8.1,2.3,6,4.4,6,7
+		c0,2.1,1.4,3.8,3.2,4.4C5.1,12.1,2,16,2,20.7c0,0.4,0.3,0.7,0.7,0.7s0.7-0.3,0.7-0.7c0-4.5,3.3-8.2,7.4-8.2c1.3,0,2.5,0.4,3.6,1.1
+		C14.4,13.3,14.5,13.1,14.5,13.1z M10.6,10.3c-1.9,0-3.4-1.5-3.4-3.3c0-1.9,1.5-3.4,3.4-3.4S14,5.1,14,7S12.5,10.3,10.6,10.3z"/>
+</g>
 </svg>
diff --git a/resources/icons/gotoconversation.svg b/resources/icons/gotoconversation.svg
new file mode 100644
index 000000000..11b527139
--- /dev/null
+++ b/resources/icons/gotoconversation.svg
@@ -0,0 +1,13 @@
+<?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="M22,12.5c0-1.9-0.9-3.8-2.6-5.1l0,0c-0.3-0.2-0.6-0.5-1-0.7c-0.4-0.7-1-1.3-1.7-1.8c-1.6-1.3-3.8-2-6.1-2s-4.4,0.7-6.1,2
+	C2.9,6.2,2,8,2,10c0,1.9,0.9,3.7,2.5,5l-0.1,3c0,0.3,0.3,0.6,0.6,0.6c0.1,0,0.2,0,0.3,0l2.1-0.9c1.6,1.2,3.7,1.9,6,1.9l0,0
+	c0.5,0,1.1,0,1.7-0.1l3.8,1.6c0.1,0,0.2,0,0.2,0c0.1,0,0.2,0,0.2,0c0.2-0.1,0.3-0.2,0.3-0.3c0-0.1,0.1-0.2,0-0.3l-0.1-3
+	C21.1,16.1,22,14.4,22,12.5z M18.4,19.6l-3.1-1.3c-0.1,0-0.2,0-0.2,0H15c-0.5,0.1-1.1,0.1-1.6,0.1c-1.7,0-3.3-0.4-4.6-1.3L9,17
+	c0.5,0.1,1.1,0.1,1.7,0.1c2.3,0,4.4-0.7,6.1-2c1.7-1.3,2.6-3.2,2.6-5.1c0-0.3,0-0.7-0.1-1c1,1,1.5,2.2,1.5,3.5c0,1.6-0.8,3-2.2,4.1
+	l-0.1,0.1c-0.2,0.1-0.2,0.3-0.2,0.5L18.4,19.6z M5.5,14.2L5.5,14.2C4,13,3.2,11.6,3.2,10S4,7,5.4,5.9s3.3-1.7,5.3-1.7
+	s3.9,0.6,5.3,1.7s2.2,2.6,2.2,4.1c0,1.6-0.8,3-2.2,4.1c-1.4,1.1-3.3,1.7-5.3,1.7c-0.5,0-1.1,0-1.6-0.1c-0.1,0-0.2,0-0.3,0L5.7,17
+	l0.1-2.3C5.7,14.5,5.7,14.3,5.5,14.2z"/>
+</svg>
diff --git a/resources/icons/kick_member.svg b/resources/icons/kick_member.svg
new file mode 100644
index 000000000..00d0f9372
--- /dev/null
+++ b/resources/icons/kick_member.svg
@@ -0,0 +1,14 @@
+<?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">
+<g>
+	<path d="M14.5,13.1l0.7-0.4c-1-0.7-2.1-1.2-3.3-1.4c1.9-0.6,3.2-2.3,3.2-4.4c0.1-2.6-2-4.6-4.5-4.6C8.1,2.3,6,4.4,6,7
+		c0,2.1,1.4,3.8,3.2,4.4C5.1,12.1,2,16,2,20.7c0,0.4,0.3,0.7,0.7,0.7s0.7-0.3,0.7-0.7c0-4.5,3.3-8.2,7.4-8.2c1.3,0,2.5,0.4,3.6,1.1
+		C14.4,13.3,14.5,13.1,14.5,13.1z M10.6,10.3c-1.9,0-3.4-1.5-3.4-3.3c0-1.9,1.5-3.4,3.4-3.4S14,5.1,14,7S12.5,10.3,10.6,10.3z"/>
+	<path d="M16.9,11.5c-2.8,0-5.1,2.3-5.1,5.1c0,2.8,2.3,5.1,5.1,5.1c2.8,0,5.1-2.3,5.1-5.1C22,13.8,19.7,11.5,16.9,11.5z M20.3,18.4
+		L18,16.3l2.2-1.9c0.4,0.6,0.7,1.4,0.7,2.2C20.8,17.3,20.6,17.9,20.3,18.4z M19.3,13.5l-2.2,2l-2.3-2.1c0.6-0.4,1.4-0.7,2.2-0.7
+		C17.8,12.7,18.6,13,19.3,13.5z M13.9,14.3l2.3,2l-2.5,2.3c-0.3-0.6-0.6-1.3-0.6-2C13.1,15.7,13.4,14.9,13.9,14.3z M14.4,19.5
+		l2.7-2.4l2.5,2.3c-0.7,0.7-1.6,1.1-2.7,1.1C16,20.5,15.1,20.1,14.4,19.5z"/>
+</g>
+</svg>
diff --git a/src/contactadapter.cpp b/src/contactadapter.cpp
index 49ad99911..6c96c6d1c 100644
--- a/src/contactadapter.cpp
+++ b/src/contactadapter.cpp
@@ -239,6 +239,13 @@ ContactAdapter::contactSelected(int index)
     }
 }
 
+void
+ContactAdapter::removeContact(const QString& peerUri, bool banContact)
+{
+    auto& accInfo = lrcInstance_->getCurrentAccountInfo();
+    accInfo.contactModel->removeContact(peerUri, banContact);
+}
+
 void
 ContactAdapter::connectSignals()
 {
diff --git a/src/contactadapter.h b/src/contactadapter.h
index c736816ff..d70f140b0 100644
--- a/src/contactadapter.h
+++ b/src/contactadapter.h
@@ -91,6 +91,7 @@ public:
     Q_INVOKABLE QVariant getContactSelectableModel(int type);
     Q_INVOKABLE void setSearchFilter(const QString& filter);
     Q_INVOKABLE void contactSelected(int index);
+    Q_INVOKABLE void removeContact(const QString& peerUri, bool banContact);
 
     void connectSignals();
 
@@ -104,7 +105,8 @@ private:
 
     QStringList defaultModerators_;
 
-    bool hasDifferentMembers(const VectorString& currentMembers, const VectorString& convMembers) const;
+    bool hasDifferentMembers(const VectorString& currentMembers,
+                             const VectorString& convMembers) const;
 
 Q_SIGNALS:
     void defaultModeratorsUpdated();
diff --git a/src/conversationsadapter.cpp b/src/conversationsadapter.cpp
index a08c82af7..509578a2e 100644
--- a/src/conversationsadapter.cpp
+++ b/src/conversationsadapter.cpp
@@ -381,6 +381,7 @@ ConversationsAdapter::setFilter(const QString& filterString)
 {
     convModel_->setFilter(filterString);
     searchSrcModel_->setFilter(filterString);
+    Q_EMIT textFilterChanged(filterString);
 }
 
 QVariantMap
@@ -499,6 +500,24 @@ ConversationsAdapter::updateConversationDescription(const QString& convId,
     convModel->updateConversationInfo(convId, details);
 }
 
+QString
+ConversationsAdapter::dialogId(const QString& peerUri)
+{
+    auto& convInfo = lrcInstance_->getConversationFromPeerUri(peerUri);
+    if (!convInfo.uid.isEmpty() && convInfo.isCoreDialog())
+        return convInfo.uid;
+    return {};
+}
+
+void
+ConversationsAdapter::openDialogConversationWith(const QString& peerUri)
+{
+    auto& convInfo = lrcInstance_->getConversationFromPeerUri(peerUri);
+    if (convInfo.uid.isEmpty() || !convInfo.isCoreDialog())
+        return;
+    lrcInstance_->selectConversation(convInfo.uid);
+}
+
 bool
 ConversationsAdapter::connectConversationModel()
 {
diff --git a/src/conversationsadapter.h b/src/conversationsadapter.h
index 1d7e318fe..15189542e 100644
--- a/src/conversationsadapter.h
+++ b/src/conversationsadapter.h
@@ -59,9 +59,12 @@ public:
     Q_INVOKABLE void updateConversationDescription(const QString& convId,
                                                    const QString& newDescription);
 
+    Q_INVOKABLE QString dialogId(const QString& peerUri);
+    Q_INVOKABLE void openDialogConversationWith(const QString& peerUri);
 Q_SIGNALS:
     void showConversation(const QString& accountId, const QString& convUid);
     void showSearchStatus(const QString& status);
+    void textFilterChanged(const QString& text);
 
     void navigateToWelcomePageRequested();
     void conversationReady(const QString& convId);
diff --git a/src/mainview/components/SidePanel.qml b/src/mainview/components/SidePanel.qml
index 9fbe8d0e1..894bb9f60 100644
--- a/src/mainview/components/SidePanel.qml
+++ b/src/mainview/components/SidePanel.qml
@@ -156,6 +156,12 @@ Rectangle {
         function onShowSearchStatus(status) {
             searchStatusText.text = status
         }
+
+        function onTextFilterChanged(text) {
+            // In the swarm details, "Go to conversation" can
+            // change the search bar. Be sure to be synced
+            contactSearchBar.textContent = text
+        }
     }
 
     ColumnLayout {
diff --git a/src/mainview/components/SwarmDetailsPanel.qml b/src/mainview/components/SwarmDetailsPanel.qml
index c92e9a917..f5adf0f10 100644
--- a/src/mainview/components/SwarmDetailsPanel.qml
+++ b/src/mainview/components/SwarmDetailsPanel.qml
@@ -168,6 +168,7 @@ Rectangle {
 
                 SwarmParticipantContextMenu {
                     id: contextMenu
+                    role: UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, CurrentAccount.uri)
 
                     function openMenuAt(x, y, participantUri) {
                         contextMenu.x = x
@@ -187,6 +188,7 @@ Rectangle {
 
                     MouseArea {
                         anchors.fill: parent
+                        enabled: modelData != CurrentAccount.uri
                         acceptedButtons: Qt.RightButton
                         onClicked: function (mouse) {
                             contextMenu.openMenuAt(x + mouse.x, y + mouse.y, modelData)
diff --git a/src/mainview/components/SwarmParticipantContextMenu.qml b/src/mainview/components/SwarmParticipantContextMenu.qml
index 4dcc5ce57..d162b7371 100644
--- a/src/mainview/components/SwarmParticipantContextMenu.qml
+++ b/src/mainview/components/SwarmParticipantContextMenu.qml
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 by Savoir-faire Linux
+ * Copyright (C) 2022 by Savoir-faire Linux
  * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
  *
  * This program is free software; you can redistribute it and/or modify
@@ -29,44 +29,55 @@ ContextMenuAutoLoader {
     id: root
     property var conversationId: ""
     property var participantUri: ""
-
-    // TODO get authorization
+    property var role
 
     property list<GeneralMenuItem> menuItems: [
         GeneralMenuItem {
             id: startVideoCallItem
             itemName: JamiStrings.startVideoCall
+            canTrigger: ConversationsAdapter.dialogId(participantUri) !== ""
+            iconSource: JamiResources.videocam_24dp_svg
             onClicked: {
+                ConversationsAdapter.openDialogConversationWith(participantUri)
+                CallAdapter.placeCall()
             }
         },
         GeneralMenuItem {
             id: startAudioCall
             itemName: JamiStrings.startAudioCall
+            canTrigger: ConversationsAdapter.dialogId(participantUri) !== ""
+            iconSource: JamiResources.place_audiocall_24dp_svg
             onClicked: {
+                ConversationsAdapter.openDialogConversationWith(participantUri)
+                CallAdapter.placeAudioOnlyCall()
             }
         },
         GeneralMenuItem {
             id: goToConversation
 
+            iconSource: JamiResources.gotoconversation_svg
             itemName: JamiStrings.goToConversation
             onClicked: {
+                if (ConversationsAdapter.dialogId(participantUri) !== "")
+                    ConversationsAdapter.openDialogConversationWith(participantUri)
+                else
+                    ConversationsAdapter.setFilter(participantUri)
             }
         },
-        GeneralMenuItem {
-            id: promoteAdministrator
-            canTrigger: false // No API yet
-            itemName: JamiStrings.promoteAdministrator
-        },
         GeneralMenuItem {
             id: blockContact
             itemName: JamiStrings.blockContact
             iconSource: JamiResources.block_black_24dp_svg
+            onClicked: {
+                ContactAdapter.removeContact(participantUri, true)
+            }
         },
         GeneralMenuItem {
             id: kickMember
             itemName: JamiStrings.kickMember
+            iconSource: JamiResources.kick_member_svg
+            canTrigger: role === Member.Role.ADMIN
 
-            // TODO can trigger (enough permission for self and member accepted)
             onClicked: {
                 MessagesAdapter.removeConversationMember(conversationId, participantUri)
             }
diff --git a/src/utils.cpp b/src/utils.cpp
index 0d7d2b4df..65afcd260 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -376,7 +376,6 @@ Utils::contactPhoto(LRCInstance* instance,
             photo = Utils::fallbackAvatar("jami:" + contactInfo.profileInfo.uri, avatarName);
         }
     } catch (const std::exception& e) {
-        qDebug() << e.what() << "; Using default avatar";
         photo = fallbackAvatar("jami:" + contactUri, QString(), size);
     }
     return Utils::scaleAndFrame(photo, size);
@@ -401,14 +400,14 @@ Utils::conversationAvatar(LRCInstance* instance,
             return avatar;
         if (members.size() == 1) {
             // Only member in the swarm or 1:1, draw only peer's avatar
-            auto peerAvatar = Utils::contactPhoto(instance, members[0], size);
+            auto peerAvatar = Utils::contactPhoto(instance, members[0], size, "");
             painter.drawImage(avatar.rect(), peerAvatar);
             return avatar;
         }
         // Else, combine avatars
         auto idx = 0;
-        auto peerAAvatar = Utils::contactPhoto(instance, members[0], size);
-        auto peerBAvatar = Utils::contactPhoto(instance, members[1], size);
+        auto peerAAvatar = Utils::contactPhoto(instance, members[0], size, "");
+        auto peerBAvatar = Utils::contactPhoto(instance, members[1], size, "");
         peerAAvatar = Utils::halfCrop(peerAAvatar, true);
         peerBAvatar = Utils::halfCrop(peerBAvatar, false);
         painter.drawImage(avatar.rect(), peerAAvatar);
-- 
GitLab