diff --git a/src/avatarimageprovider.h b/src/avatarimageprovider.h index 285429a1..6e5a7cb3 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 8bd97b3f..aa0072c7 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 828ab91f..c5c8d2f2 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 32a2589e..cb80596a 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 0b4e4026..900f4207 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_; diff --git a/src/mainview/MainView.qml b/src/mainview/MainView.qml index 0f8008ab..294d658a 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 79dfdd5e..58b576bb 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 db416097..25bf1b11 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) } - highlightedMembers = result + + // We can't have more than 8 participants yet. + if (newHm.length > 8) { + return false + } + + 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 f5adf0f1..12e342cc 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 65afcd26..3720d54a 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 cf6fd8ee..174dfff7 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 300dc629..f224f4bd 100644 --- a/src/utilsadapter.cpp +++ b/src/utilsadapter.cpp @@ -31,6 +31,7 @@ #include "api/datatransfermodel.h" #include +#include #include #include #include @@ -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 cf1164fc..7748a07e 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);