From a5cfffef6dc3665c1ce5afea85f217c47d27d314 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Fri, 11 Feb 2022 16:25:42 -0500
Subject: [PATCH] swarm creation: add ability to change avatar

PhotoBoothView has a new variable to be used during Swarm's creation
This update an image in the cache and is used in the profile of the
conversation.
Also, add top bar for NewSwarmPage

Change-Id: I156c9cffb85e15b7c041bcf16b1501851470e8a5
GitLab: #670
---
 src/avatarimageprovider.h                     |  7 +-
 src/avatarregistry.cpp                        | 10 +++
 src/commoncomponents/PhotoboothView.qml       | 33 +++++--
 src/constant/JamiStrings.qml                  |  1 +
 src/lrcinstance.h                             |  1 +
 src/mainview/MainView.qml                     |  8 ++
 src/mainview/components/NewSwarmPage.qml      | 87 ++++++++++++++++++-
 src/mainview/components/SidePanel.qml         | 72 ++++++++++++---
 src/mainview/components/SwarmDetailsPanel.qml | 10 +--
 src/utils.cpp                                 | 14 +++
 src/utils.h                                   |  1 +
 src/utilsadapter.cpp                          | 56 ++++++++++++
 src/utilsadapter.h                            |  8 ++
 13 files changed, 278 insertions(+), 30 deletions(-)

diff --git a/src/avatarimageprovider.h b/src/avatarimageprovider.h
index 285429a1c..6e5a7cb38 100644
--- a/src/avatarimageprovider.h
+++ b/src/avatarimageprovider.h
@@ -60,9 +60,12 @@ public:
         }
 
         auto type = idInfo.at(0);
-        if (type == "conversation")
+        if (type == "conversation") {
+            if (imageId == "temp")
+                return Utils::tempConversationAvatar(requestedSize);
+
             return Utils::conversationAvatar(lrcInstance_, imageId, requestedSize);
-        else if (type == "account")
+        } else if (type == "account")
             return Utils::accountPhoto(lrcInstance_, imageId, requestedSize);
         else if (type == "contact")
             return Utils::contactPhoto(lrcInstance_, imageId, requestedSize);
diff --git a/src/avatarregistry.cpp b/src/avatarregistry.cpp
index 8bd97b3fa..aa0072c72 100644
--- a/src/avatarregistry.cpp
+++ b/src/avatarregistry.cpp
@@ -35,6 +35,10 @@ AvatarRegistry::AvatarRegistry(LRCInstance* instance, QObject* parent)
             &AvatarRegistry::addOrUpdateImage,
             Qt::UniqueConnection);
 
+    connect(lrcInstance_, &LRCInstance::base64SwarmAvatarChanged, this, [&] {
+        addOrUpdateImage("temp");
+    });
+
     if (!lrcInstance_->get_currentAccountId().isEmpty())
         connectAccount();
 }
@@ -62,6 +66,12 @@ AvatarRegistry::connectAccount()
             this,
             &AvatarRegistry::onProfileUpdated,
             Qt::UniqueConnection);
+
+    connect(lrcInstance_->getCurrentConversationModel(),
+            &ConversationModel::conversationUpdated,
+            this,
+            &AvatarRegistry::addOrUpdateImage,
+            Qt::UniqueConnection);
 }
 
 void
diff --git a/src/commoncomponents/PhotoboothView.qml b/src/commoncomponents/PhotoboothView.qml
index 828ab91fa..c5c8d2f24 100644
--- a/src/commoncomponents/PhotoboothView.qml
+++ b/src/commoncomponents/PhotoboothView.qml
@@ -30,6 +30,7 @@ Item {
 
     property bool isPreviewing: false
     property alias imageId: avatar.imageId
+    property bool newConversation: false
     property real avatarSize
 
     signal focusOnPreviousItem
@@ -94,7 +95,10 @@ Item {
             }
 
             var filePath = UtilsAdapter.getAbsPath(file)
-            AccountAdapter.setCurrentAccountAvatarFile(filePath)
+            if (!root.newConversation)
+                AccountAdapter.setCurrentAccountAvatarFile(filePath)
+            else
+                UtilsAdapter.setSwarmCreationImageFromFile(filePath, root.imageId)
         }
 
         onRejected: {
@@ -125,6 +129,8 @@ Item {
 
                 visible: !preview.visible
 
+                mode: newConversation? Avatar.Mode.Conversation : Avatar.Mode.Account
+
                 fillMode: Image.PreserveAspectCrop
                 showPresenceIndicator: false
             }
@@ -220,8 +226,11 @@ Item {
                 onClicked: {
                     if (isPreviewing) {
                         flashAnimation.start()
-                        AccountAdapter.setCurrentAccountAvatarBase64(
-                                    preview.takePhoto(avatarSize))
+                        var photo = preview.takePhoto(avatarSize)
+                        if (!root.newConversation)
+                            AccountAdapter.setCurrentAccountAvatarBase64(photo)
+                        else
+                            UtilsAdapter.setSwarmCreationImageFromString(photo, imageId)
                         stopBooth()
                         return
                     }
@@ -237,7 +246,15 @@ Item {
 
                 Layout.alignment: Qt.AlignHCenter
 
-                visible: isPreviewing || LRCInstance.currentAccountAvatarSet
+                visible: {
+                    if (isPreviewing)
+                        return true
+                    if (!newConversation && LRCInstance.currentAccountAvatarSet)
+                        return true
+                    if (newConversation && UtilsAdapter.swarmCreationImage(imageId).length !== 0)
+                        return true
+                    return false
+                }
 
                 radius: JamiTheme.primaryRadius
                 source: JamiResources.round_close_24dp_svg
@@ -265,8 +282,12 @@ Item {
 
                 onClicked: {
                     stopBooth()
-                    if (!isPreviewing)
-                        AccountAdapter.setCurrentAccountAvatarBase64()
+                    if (!isPreviewing) {
+                        if (!root.newConversation)
+                            AccountAdapter.setCurrentAccountAvatarBase64()
+                        else
+                            UtilsAdapter.setSwarmCreationImageFromString("", imageId)
+                    }
                 }
             }
 
diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml
index 32a2589ef..cb80596a3 100644
--- a/src/constant/JamiStrings.qml
+++ b/src/constant/JamiStrings.qml
@@ -629,4 +629,5 @@ Item {
     property string kickMember: qsTr("Kick member")
     property string administrator: qsTr("Administrator")
     property string invited: qsTr("Invited")
+    property string removeMember: qsTr("Remove member")
 }
diff --git a/src/lrcinstance.h b/src/lrcinstance.h
index 0b4e40269..900f4207f 100644
--- a/src/lrcinstance.h
+++ b/src/lrcinstance.h
@@ -133,6 +133,7 @@ Q_SIGNALS:
     void quitEngineRequested();
     void conversationUpdated(const QString& convId, const QString& accountId);
     void draftSaved(const QString& convId);
+    void base64SwarmAvatarChanged();
 
 private:
     std::unique_ptr<Lrc> lrc_;
diff --git a/src/mainview/MainView.qml b/src/mainview/MainView.qml
index 0f8008ab9..294d658a9 100644
--- a/src/mainview/MainView.qml
+++ b/src/mainview/MainView.qml
@@ -376,6 +376,10 @@ Rectangle {
                 pushNewSwarmPage()
             }
         }
+
+        onHighlightedMembersChanged: {
+            newSwarmPage.members = mainViewSidePanel.highlightedMembers
+        }
     }
 
     CallStackView {
@@ -426,6 +430,10 @@ Rectangle {
             mainViewSidePanel.showSwarmListView(newSwarmPage.visible)
         }
 
+        onRemoveMember: function(convId, member) {
+            mainViewSidePanel.removeMember(convId, member)
+        }
+
         onCreateSwarmClicked: function(title, description, avatar) {
             ConversationsAdapter.createSwarm(title, description, avatar, mainViewSidePanel.highlightedMembers)
             backToMainView()
diff --git a/src/mainview/components/NewSwarmPage.qml b/src/mainview/components/NewSwarmPage.qml
index 79dfdd5e1..58b576bbf 100644
--- a/src/mainview/components/NewSwarmPage.qml
+++ b/src/mainview/components/NewSwarmPage.qml
@@ -33,12 +33,95 @@ Rectangle {
     color: JamiTheme.chatviewBgColor
 
     signal createSwarmClicked(string title, string description, string avatar)
+    signal removeMember(string convId, string member)
+
+    onVisibleChanged: {
+        UtilsAdapter.setSwarmCreationImageFromString()
+    }
+
+    property var members: []
+
+    RowLayout {
+        id: labelsMember
+        anchors.top: root.top
+        anchors.topMargin: 16
+        anchors.leftMargin: 16
+        Layout.preferredWidth: root.width
+        spacing: 16
+
+        Label {
+            text: qsTr("To:")
+            font.bold: true
+            color: JamiTheme.textColor
+        }
+
+        ScrollView {
+            Layout.preferredWidth: root.width
+            Layout.fillWidth: true
+            Layout.preferredHeight: 48
+            Layout.topMargin: 16
+            clip: true
+
+            RowLayout {
+                anchors.fill: parent
+                Repeater {
+                    id: repeater
+
+                    delegate: Rectangle {
+                        id: delegate
+                        radius: (delegate.height + 12) / 2
+                        width: childrenRect.width + 12
+                        height: childrenRect.height + 12
+
+                        RowLayout {
+                            anchors.centerIn: parent
+
+                            Label {
+                                text: UtilsAdapter.getBestNameForUri(CurrentAccount.id, modelData.uri)
+                                color: JamiTheme.textColor
+                            }
+
+                            PushButton {
+                                id: removeUserBtn
+
+                                Layout.leftMargin: 8
+
+                                preferredSize: 24
+
+                                source: JamiResources.round_close_24dp_svg
+                                toolTipText: JamiStrings.removeMember
+
+                                normalColor: "transparent"
+                                imageColor: "transparent"
+
+                                onClicked: root.removeMember(modelData.convId, modelData.uri)
+                            }
+                        }
+
+                        color: "grey"
+                    }
+                    model: root.members
+                }
+            }
+        }
+
+
+    }
 
     ColumnLayout {
         id: mainLayout
-
         anchors.centerIn: root
 
+        PhotoboothView {
+            id: currentAccountAvatar
+
+            Layout.alignment: Qt.AlignCenter
+
+            newConversation: true
+            imageId: root.visible ? "temp" : ""
+            avatarSize: 180
+        }
+
         EditableLineEdit {
             id: title
             Layout.alignment: Qt.AlignCenter
@@ -83,7 +166,7 @@ Rectangle {
             text: JamiStrings.createTheSwarm
 
             onClicked: {
-                createSwarmClicked(title.text, description.text, "")
+                createSwarmClicked(title.text, description.text, UtilsAdapter.swarmCreationImage())
             }
         }
     }
diff --git a/src/mainview/components/SidePanel.qml b/src/mainview/components/SidePanel.qml
index db4160977..25bf1b11d 100644
--- a/src/mainview/components/SidePanel.qml
+++ b/src/mainview/components/SidePanel.qml
@@ -62,20 +62,66 @@ Rectangle {
     property var highlighted: []
     property var highlightedMembers: []
 
-    function refreshHighlighted() {
-        var result = []
-        for (var idx in highlighted) {
-            var convId = highlighted[idx]
+    function refreshHighlighted(convId, highlightedStatus) {
+        var newH = root.highlighted
+        var newHm = root.highlightedMembers
+
+        if (highlightedStatus) {
             var item = ConversationsAdapter.getConvInfoMap(convId)
+            var added = false
             for (var idx in item.uris) {
                 var uri = item.uris[idx]
-                if (!result.indexOf(uri) != -1 && uri != CurrentAccount.uri) {
-                    result.push(uri)
+                if (!Array.from(newHm).find(r => r.uri === uri) && uri != CurrentAccount.uri) {
+                    newHm.push({"uri": uri, "convId": convId})
+                    added = true
                 }
             }
+            if (!added)
+                return false
+        } else {
+            newH = Array.from(newH).filter(r => r !== convId)
+            newHm = Array.from(newHm).filter(r => r.convId !== convId)
+        }
+
+        // We can't have more than 8 participants yet.
+        if (newHm.length > 8) {
+            return false
         }
-        highlightedMembers = result
+
+        newH.push(convId)
+        root.highlighted = newH
+        root.highlightedMembers = newHm
         ConversationsAdapter.ignoreFiltering(root.highlighted)
+        return true
+    }
+
+    function clearHighlighted() {
+        root.highlighted = []
+        root.highlightedMembers = []
+    }
+
+    function removeMember(convId, member) {
+        var refreshHighlighted = true
+        var newHm = []
+        for (var hm in root.highlightedMembers) {
+            var m = root.highlightedMembers[hm]
+            if (m.convId == convId && m.uri == member) {
+                continue;
+            } else if (m.convId == convId) {
+                refreshHighlighted = false
+            }
+            newHm.push(m)
+        }
+        root.highlightedMembers = newHm
+
+        if (refreshHighlighted) {
+            // Remove highlighted status if necessary
+            for (var d in swarmCurrentConversationList.contentItem.children) {
+                var delegate = swarmCurrentConversationList.contentItem.children[d]
+                if (delegate.convId == convId)
+                    delegate.highlighted = false
+            }
+        }
     }
 
     function showSwarmListView(v) {
@@ -280,23 +326,21 @@ Rectangle {
                 onVisibleChanged: {
                     if (!visible) {
                         highlighted = false
-                        root.refreshHighlighted()
+                        root.clearHighlighted()
                     }
                 }
 
                 onHighlightedChanged: function onHighlightedChanged() {
                     var currentHighlighted = root.highlighted
+                    if (!root.refreshHighlighted(convId, highlighted)) {
+                        highlighted = false
+                        return
+                    }
                     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/SwarmDetailsPanel.qml b/src/mainview/components/SwarmDetailsPanel.qml
index f5adf0f10..12e342ccd 100644
--- a/src/mainview/components/SwarmDetailsPanel.qml
+++ b/src/mainview/components/SwarmDetailsPanel.qml
@@ -42,18 +42,16 @@ Rectangle {
             Layout.fillWidth: true
             spacing: 0
 
-            ConversationAvatar {
-                id: conversationAvatar
+            PhotoboothView {
+                id: currentAccountAvatar
 
                 Layout.alignment: Qt.AlignCenter
-                Layout.preferredWidth: JamiTheme.avatarSizeInCall
-                Layout.preferredHeight: JamiTheme.avatarSizeInCall
                 Layout.topMargin: JamiTheme.swarmDetailsPageTopMargin
                 Layout.bottomMargin: JamiTheme.preferredMarginSize
 
+                newConversation: true
                 imageId: LRCInstance.selectedConvUid
-
-                showPresenceIndicator: false
+                avatarSize: JamiTheme.avatarSizeInCall
             }
 
             EditableLineEdit {
diff --git a/src/utils.cpp b/src/utils.cpp
index 65afcd260..3720d54ac 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -395,6 +395,10 @@ Utils::conversationAvatar(LRCInstance* instance,
         auto& accInfo = instance->accountModel().getAccountInfo(
             accountId.isEmpty() ? instance->get_currentAccountId() : accountId);
         auto* convModel = accInfo.conversationModel.get();
+        auto avatarb64 = convModel->avatar(convId);
+        if (!avatarb64.isEmpty())
+            return scaleAndFrame(imageFromBase64String(avatarb64, true), size);
+        // Else, generate an avatar
         auto members = convModel->peersForConversation(convId);
         if (members.size() < 1)
             return avatar;
@@ -418,6 +422,16 @@ Utils::conversationAvatar(LRCInstance* instance,
     return avatar;
 }
 
+QImage
+Utils::tempConversationAvatar(const QSize& size)
+{
+    QString img = QByteArrayFromFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
+                                     + "tmpSwarmImage");
+    if (img.isEmpty())
+        return fallbackAvatar(QString(), QString(), size);
+    return scaleAndFrame(imageFromBase64String(img, true), size);
+}
+
 QImage
 Utils::imageFromBase64String(const QString& str, bool circleCrop)
 {
diff --git a/src/utils.h b/src/utils.h
index cf6fd8ee0..174dfff7e 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -97,6 +97,7 @@ QImage conversationAvatar(LRCInstance* instance,
 QImage getCirclePhoto(const QImage original, int sizePhoto);
 QImage halfCrop(const QImage original, bool leftSide);
 QColor getAvatarColor(const QString& canonicalUri);
+QImage tempConversationAvatar(const QSize& size);
 QImage fallbackAvatar(const QString& canonicalUriStr,
                       const QString& letterStr = {},
                       const QSize& size = defaultAvatarSize);
diff --git a/src/utilsadapter.cpp b/src/utilsadapter.cpp
index 300dc6297..f224f4bdc 100644
--- a/src/utilsadapter.cpp
+++ b/src/utilsadapter.cpp
@@ -31,6 +31,7 @@
 #include "api/datatransfermodel.h"
 
 #include <QApplication>
+#include <QBuffer>
 #include <QClipboard>
 #include <QFileInfo>
 #include <QRegExp>
@@ -135,6 +136,12 @@ UtilsAdapter::getBestName(const QString& accountId, const QString& uid)
     return QString();
 }
 
+QString
+UtilsAdapter::getBestNameForUri(const QString& accountId, const QString& uri)
+{
+    return lrcInstance_->getAccountInfo(accountId).contactModel->bestNameForContact(uri);
+}
+
 const QString
 UtilsAdapter::getPeerUri(const QString& accountId, const QString& uid)
 {
@@ -472,6 +479,55 @@ UtilsAdapter::supportedLang()
     return result;
 }
 
+QString
+UtilsAdapter::swarmCreationImage(const QString& imageId) const
+{
+    if (imageId == "temp")
+        return Utils::QByteArrayFromFile(
+            QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "tmpSwarmImage");
+    return lrcInstance_->getCurrentConversationModel()->avatar(imageId);
+}
+
+void
+UtilsAdapter::setSwarmCreationImageFromString(const QString& image, const QString& imageId)
+{
+    // Compress the image before saving
+    auto img = Utils::imageFromBase64String(image, false);
+    setSwarmCreationImageFromImage(img);
+}
+
+void
+UtilsAdapter::setSwarmCreationImageFromFile(const QString& path, const QString& imageId)
+{
+    // Compress the image before saving
+    auto image = Utils::QByteArrayFromFile(path);
+    auto img = Utils::imageFromBase64Data(image, false);
+    setSwarmCreationImageFromImage(img);
+}
+
+void
+UtilsAdapter::setSwarmCreationImageFromImage(const QImage& image, const QString& imageId)
+{
+    // Compress the image before saving
+    auto img = Utils::scaleAndFrame(image, QSize(256, 256));
+    QByteArray ba;
+    QBuffer bu(&ba);
+    img.save(&bu, "PNG");
+    // Save the image
+    if (imageId == "temp") {
+        QFile file(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
+                   + "tmpSwarmImage");
+        file.open(QIODevice::WriteOnly);
+        file.write(ba.toBase64());
+        file.close();
+        Q_EMIT lrcInstance_->base64SwarmAvatarChanged();
+    } else {
+        lrcInstance_->getCurrentConversationModel()->updateConversationInfo(imageId,
+                                                                            {{"avatar",
+                                                                              ba.toBase64()}});
+    }
+}
+
 bool
 UtilsAdapter::getContactPresence(const QString& accountId, const QString& uri)
 {
diff --git a/src/utilsadapter.h b/src/utilsadapter.h
index cf1164fcf..7748a07e1 100644
--- a/src/utilsadapter.h
+++ b/src/utilsadapter.h
@@ -58,6 +58,7 @@ public:
     Q_INVOKABLE bool checkStartupLink();
     Q_INVOKABLE void setConversationFilter(const QString& filter);
     Q_INVOKABLE const QString getBestName(const QString& accountId, const QString& uid);
+    Q_INVOKABLE QString getBestNameForUri(const QString& accountId, const QString& uri);
     Q_INVOKABLE const QString getPeerUri(const QString& accountId, const QString& uid);
     Q_INVOKABLE QString getBestId(const QString& accountId);
     Q_INVOKABLE const QString getBestId(const QString& accountId, const QString& uid);
@@ -91,6 +92,13 @@ public:
     Q_INVOKABLE void monitor(const bool& continuous);
     Q_INVOKABLE void clearInteractionsCache(const QString& accountId, const QString& convUid);
     Q_INVOKABLE QVariantMap supportedLang();
+    Q_INVOKABLE QString swarmCreationImage(const QString& imageId = "temp") const;
+    Q_INVOKABLE void setSwarmCreationImageFromString(const QString& image = "",
+                                                     const QString& imageId = "temp");
+    Q_INVOKABLE void setSwarmCreationImageFromFile(const QString& path,
+                                                   const QString& imageId = "temp");
+    Q_INVOKABLE void setSwarmCreationImageFromImage(const QImage& image,
+                                                   const QString& imageId = "temp");
 
     // For Swarm details page
     Q_INVOKABLE bool getContactPresence(const QString& accountId, const QString& uri);
-- 
GitLab