From 4bda3306374e80a8ccda84127839551ac8a32228 Mon Sep 17 00:00:00 2001 From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> Date: Tue, 29 Jun 2021 10:39:06 -0400 Subject: [PATCH] swarm: simplify and update avatar update mechanism Implements a leaner avatar caching system. The avatar component listens for uid filtering its id, which may be: - conversation id - account id - contact uri In response to the uid change, a the image source is updated with a new image url invoking a fresh QQuickImageProvider query. With this design, only the avatarregistry's uid mapping needs to be updated when profiles are changed, and no longer should specific avatar components receive manual source updates. Gitlab: #466 Change-Id: Ie5313f5c187a0977ca51b890dd92187480a42537 --- CMakeLists.txt | 6 +- qml.qrc | 3 +- src/accountadapter.cpp | 33 ++- src/accountadapter.h | 3 +- src/accountlistmodel.cpp | 27 -- src/accountlistmodel.h | 20 +- src/avatarimageprovider.h | 60 ++-- src/avatarregistry.cpp | 84 ++++++ src/avatarregistry.h | 51 ++++ src/calloverlaymodel.cpp | 2 - .../AccountMigrationDialog.qml | 17 +- src/commoncomponents/Avatar.qml | 118 ++++++++ src/commoncomponents/AvatarImage.qml | 229 ---------------- src/commoncomponents/PhotoboothView.qml | 258 ++++++------------ src/commoncomponents/SpinningAnimation.qml | 20 +- src/constant/JamiStrings.qml | 3 +- src/constant/JamiTheme.qml | 1 + src/conversationlistmodelbase.cpp | 36 --- src/conversationlistmodelbase.h | 10 - src/conversationsadapter.cpp | 10 +- src/lrcinstance.cpp | 28 -- src/lrcinstance.h | 3 - src/mainapplication.cpp | 4 + src/mainview/MainView.qml | 8 - src/mainview/components/AccountComboBox.qml | 19 +- .../components/AccountItemDelegate.qml | 5 +- .../components/ContactPickerItemDelegate.qml | 15 +- .../components/ConversationAvatar.qml | 49 ++++ .../components/ConversationListView.qml | 2 +- src/mainview/components/InitialCallPage.qml | 11 +- src/mainview/components/OngoingCallPage.qml | 5 +- .../ParticipantCallInStatusDelegate.qml | 6 +- .../components/ParticipantOverlay.qml | 30 +- .../components/SmartListItemDelegate.qml | 30 +- src/mainview/components/UserProfile.qml | 11 +- src/previewrenderer.cpp | 13 +- src/previewrenderer.h | 10 +- src/quickimageproviderbase.h | 4 +- src/searchresultslistmodel.cpp | 1 - src/selectablelistproxymodel.cpp | 8 - src/selectablelistproxymodel.h | 4 - .../components/AccountProfile.qml | 10 +- .../components/ContactItemDelegate.qml | 33 +-- .../components/CurrentAccountSettings.qml | 2 - src/smartlistmodel.cpp | 2 - src/utils.cpp | 168 ++++++------ src/utils.h | 17 +- src/wizardview/WizardView.qml | 1 - src/wizardview/components/ProfilePage.qml | 16 +- 49 files changed, 632 insertions(+), 874 deletions(-) create mode 100644 src/avatarregistry.cpp create mode 100644 src/avatarregistry.h create mode 100644 src/commoncomponents/Avatar.qml delete mode 100644 src/commoncomponents/AvatarImage.qml create mode 100644 src/mainview/components/ConversationAvatar.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 1540e2c82..0d451969e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,7 +73,8 @@ set(COMMON_SOURCES ${SRC_DIR}/conversationlistmodel.cpp ${SRC_DIR}/searchresultslistmodel.cpp ${SRC_DIR}/calloverlaymodel.cpp - ${SRC_DIR}/filestosendlistmodel.cpp) + ${SRC_DIR}/filestosendlistmodel.cpp + ${SRC_DIR}/avatarregistry.cpp) set(COMMON_HEADERS ${SRC_DIR}/avatarimageprovider.h @@ -130,7 +131,8 @@ set(COMMON_HEADERS ${SRC_DIR}/conversationlistmodel.h ${SRC_DIR}/searchresultslistmodel.h ${SRC_DIR}/calloverlaymodel.h - ${SRC_DIR}/filestosendlistmodel.h) + ${SRC_DIR}/filestosendlistmodel.h + ${SRC_DIR}/avatarregistry.h) set(QML_LIBS Qt5::Quick diff --git a/qml.qrc b/qml.qrc index de596c356..d6c7e7dd4 100644 --- a/qml.qrc +++ b/qml.qrc @@ -29,7 +29,6 @@ <file>src/commoncomponents/SimpleMessageDialog.qml</file> <file>src/commoncomponents/ResponsiveImage.qml</file> <file>src/commoncomponents/PresenceIndicator.qml</file> - <file>src/commoncomponents/AvatarImage.qml</file> <file>src/commoncomponents/DaemonReconnectPopup.qml</file> <file>src/commoncomponents/SpinningAnimation.qml</file> <file>src/settingsview/SettingsView.qml</file> @@ -155,5 +154,7 @@ <file>src/mainview/components/FilesToSendDelegate.qml</file> <file>src/mainview/components/MessageBar.qml</file> <file>src/mainview/components/FilesToSendContainer.qml</file> + <file>src/commoncomponents/Avatar.qml</file> + <file>src/mainview/components/ConversationAvatar.qml</file> </qresource> </RCC> diff --git a/src/accountadapter.cpp b/src/accountadapter.cpp index 7d9bc12c2..f65dd4c96 100644 --- a/src/accountadapter.cpp +++ b/src/accountadapter.cpp @@ -272,18 +272,31 @@ AccountAdapter::setCurrAccDisplayName(const QString& text) } void -AccountAdapter::setCurrAccAvatar(bool fromFile, const QString& source) +AccountAdapter::setCurrentAccountAvatarFile(const QString& source) { - QtConcurrent::run([this, fromFile, source]() { + QtConcurrent::run([this, source]() { QPixmap image; - bool success; - if (fromFile) - success = image.load(source); - else - success = image.loadFromData(Utils::base64StringToByteArray(source)); - - if (success) - lrcInstance_->setCurrAccAvatar(image); + if (!image.load(source)) { + qWarning() << "Not a valid image file"; + return; + } + + QByteArray ba; + QBuffer bu(&ba); + bu.open(QIODevice::WriteOnly); + image.save(&bu, "PNG"); + auto str = QString::fromLocal8Bit(ba.toBase64()); + auto accountId = lrcInstance_->get_currentAccountId(); + lrcInstance_->accountModel().setAvatar(accountId, str); + }); +} + +void +AccountAdapter::setCurrentAccountAvatarBase64(const QString& data) +{ + QtConcurrent::run([this, data]() { + auto accountId = lrcInstance_->get_currentAccountId(); + lrcInstance_->accountModel().setAvatar(accountId, data); }); } diff --git a/src/accountadapter.h b/src/accountadapter.h index 96f2dab5b..b911b7e65 100644 --- a/src/accountadapter.h +++ b/src/accountadapter.h @@ -86,7 +86,8 @@ public: Q_INVOKABLE bool hasVideoCall(); Q_INVOKABLE bool isPreviewing(); Q_INVOKABLE void setCurrAccDisplayName(const QString& text); - Q_INVOKABLE void setCurrAccAvatar(bool fromFile, const QString& source); + Q_INVOKABLE void setCurrentAccountAvatarFile(const QString& source); + Q_INVOKABLE void setCurrentAccountAvatarBase64(const QString& source); Q_SIGNALS: // Trigger other components to reconnect account related signals. diff --git a/src/accountlistmodel.cpp b/src/accountlistmodel.cpp index 33d16bcbf..672fc28fa 100644 --- a/src/accountlistmodel.cpp +++ b/src/accountlistmodel.cpp @@ -54,8 +54,6 @@ AccountListModel::data(const QModelIndex& index, int role) const auto accountId = accountList.at(index.row()); auto& accountInfo = lrcInstance_->accountModel().getAccountInfo(accountId); - // Since we are using image provider right now, image url representation should be unique to - // be able to use the image cache, account avatar will only be updated once PictureUid changed switch (role) { case Role::Alias: return QVariant(lrcInstance_->accountModel().bestNameForAccount(accountId)); @@ -67,8 +65,6 @@ AccountListModel::data(const QModelIndex& index, int role) const return QVariant(static_cast<int>(accountInfo.status)); case Role::ID: return QVariant(accountInfo.id); - case Role::PictureUid: - return avatarUidMap_[accountInfo.id]; } return QVariant(); } @@ -88,28 +84,5 @@ void AccountListModel::reset() { beginResetModel(); - fillAvatarUidMap(lrcInstance_->accountModel().getAccountList()); endResetModel(); } - -void -AccountListModel::updateAvatarUid(const QString& accountId) -{ - avatarUidMap_[accountId] = Utils::generateUid(); -} - -void -AccountListModel::fillAvatarUidMap(const QStringList& accountList) -{ - if (accountList.size() == 0) { - avatarUidMap_.clear(); - return; - } - - if (avatarUidMap_.isEmpty() || accountList.size() != avatarUidMap_.size()) { - for (int i = 0; i < accountList.size(); ++i) { - if (!avatarUidMap_.contains(accountList.at(i))) - avatarUidMap_.insert(accountList.at(i), Utils::generateUid()); - } - } -} diff --git a/src/accountlistmodel.h b/src/accountlistmodel.h index 841b92248..54d21fdc9 100644 --- a/src/accountlistmodel.h +++ b/src/accountlistmodel.h @@ -28,8 +28,7 @@ X(Username) \ X(Type) \ X(Status) \ - X(ID) \ - X(PictureUid) + X(ID) namespace AccountList { Q_NAMESPACE @@ -82,24 +81,9 @@ public: QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QHash<int, QByteArray> roleNames() const override; - /* - * This function is to reset the model when there's new account added. - */ + // reset the model when there's new account added Q_INVOKABLE void reset(); - /* - * This function is to update avatar uuid when there's an avatar changed. - */ - Q_INVOKABLE void updateAvatarUid(const QString& accountId); - protected: using Role = AccountList::Role; - -private: - /* - * Give a uuid for each account avatar and it will serve PictureUid role - */ - void fillAvatarUidMap(const QStringList& accountList); - - QMap<QString, QString> avatarUidMap_; }; diff --git a/src/avatarimageprovider.h b/src/avatarimageprovider.h index 3099b9747..3351684ad 100644 --- a/src/avatarimageprovider.h +++ b/src/avatarimageprovider.h @@ -1,6 +1,7 @@ /* - * Copyright (C) 2020 by Savoir-faire Linux + * Copyright (C) 2020-2021 by Savoir-faire Linux * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> + * Author: Andreas Traczyk <andreas.traczyk@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 @@ -33,48 +34,35 @@ public: instance) {} - /* - * Request function - * id could be - * 1. account_ + account id - * 2. file_ + file path - * 3. contact_+ contact uri - * 4. conversation_+ conversation uid - * 5. base64_ + base64 string - */ QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override { Q_UNUSED(size) + // the first string is the item uri and the second is a uid + // that is used for trigger a reload of the underlying image + // data and can be discarded at this point auto idInfo = id.split("_"); - // Id type -> account_ - auto idType = idInfo[1]; - // Id content -> every after account_ - auto idContent = id.mid(id.indexOf(idType) + idType.length() + 1); - if (idContent.isEmpty() && idType != "default") - return QImage(); + if (idInfo.size() < 2) { + qWarning() << Q_FUNC_INFO << "Missing element(s) in the image url"; + return {}; + } - if (idType == "account") { - return Utils::accountPhoto(lrcInstance_, - lrcInstance_->accountModel().getAccountInfo(idContent), - requestedSize); - } else if (idType == "conversation") { - const auto& convInfo = lrcInstance_->getConversationFromConvUid(idContent); - return Utils::contactPhoto(lrcInstance_, convInfo.participants[0], requestedSize); - } else if (idType == "contact") { - return Utils::contactPhoto(lrcInstance_, idContent, requestedSize); - } else if (idType == "fallback") { - return Utils::fallbackAvatar(idContent, QString(), requestedSize); - } else if (idType == "default") { - return Utils::fallbackAvatar(QString(), QString(), requestedSize); - } else if (idType == "base64") { - return Utils::cropImage(QImage::fromData(Utils::base64StringToByteArray(idContent))) - .scaled(requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - } else { - QImage image = QImage(idContent); - return Utils::getCirclePhoto(image, image.size().width()) - .scaled(requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + auto imageId = idInfo.at(1); + if (!imageId.size()) { + qWarning() << Q_FUNC_INFO << "Missing id in the image url"; + return {}; } + + auto type = idInfo.at(0); + if (type == "conversation") + return Utils::conversationAvatar(lrcInstance_, imageId, requestedSize); + else if (type == "account") + return Utils::accountPhoto(lrcInstance_, imageId, requestedSize); + else if (type == "contact") + return Utils::contactPhoto(lrcInstance_, imageId, requestedSize); + + qWarning() << Q_FUNC_INFO << "Missing valid prefix in the image url"; + return {}; } }; diff --git a/src/avatarregistry.cpp b/src/avatarregistry.cpp new file mode 100644 index 000000000..0cdc5fd06 --- /dev/null +++ b/src/avatarregistry.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2021 by Savoir-faire Linux + * Author: Andreas Traczyk <andreas.traczyk@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/>. + */ + +#include "avatarregistry.h" + +#include "lrcinstance.h" + +AvatarRegistry::AvatarRegistry(LRCInstance* instance, QObject* parent) + : QObject(parent) + , lrcInstance_(instance) +{ + connect(lrcInstance_, + &LRCInstance::currentAccountIdChanged, + this, + &AvatarRegistry::connectAccount); + + connect(&lrcInstance_->accountModel(), + &NewAccountModel::profileUpdated, + this, + &AvatarRegistry::addOrUpdateImage, + Qt::UniqueConnection); + + if (!lrcInstance_->get_currentAccountId().isEmpty()) + connectAccount(); +} + +QString +AvatarRegistry::addOrUpdateImage(const QString& id) +{ + auto uid = Utils::generateUid(); + auto it = uidMap_.find(id); + if (it == uidMap_.end()) { + uidMap_.insert(id, uid); + } else { + it.value() = uid; + Q_EMIT avatarUidChanged(id); + } + return uid; +} + +void +AvatarRegistry::connectAccount() +{ + connect(lrcInstance_->getCurrentContactModel(), + &ContactModel::profileUpdated, + this, + &AvatarRegistry::onProfileUpdated, + Qt::UniqueConnection); +} + +void +AvatarRegistry::onProfileUpdated(const QString& uri) +{ + auto& convInfo = lrcInstance_->getConversationFromPeerUri(uri); + if (convInfo.uid.isEmpty()) + return; + + addOrUpdateImage(convInfo.uid); +} + +QString +AvatarRegistry::getUid(const QString& id) +{ + auto it = uidMap_.find(id); + if (it == uidMap_.end()) { + return addOrUpdateImage(id); + } + return it.value(); +} diff --git a/src/avatarregistry.h b/src/avatarregistry.h new file mode 100644 index 000000000..174e154a7 --- /dev/null +++ b/src/avatarregistry.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2021 by Savoir-faire Linux + * Author: Andreas Traczyk <andreas.traczyk@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/>. + */ + +#pragma once + +#include <QObject> +#include <QMap> + +class LRCInstance; + +class AvatarRegistry : public QObject +{ + Q_OBJECT +public: + explicit AvatarRegistry(LRCInstance* instance, QObject* parent = nullptr); + ~AvatarRegistry() = default; + + // get a uid for an image in the cache + Q_INVOKABLE QString getUid(const QString& id); + + // add or update a specific image in the cache + QString addOrUpdateImage(const QString& id); + +Q_SIGNALS: + void avatarUidChanged(const QString& id); + +private Q_SLOTS: + void connectAccount(); + void onProfileUpdated(const QString& uri); + +private: + // Used to force cache updates via QQuickImageProvider + QMap<QString, QString> uidMap_; + + LRCInstance* lrcInstance_; +}; diff --git a/src/calloverlaymodel.cpp b/src/calloverlaymodel.cpp index ca15db184..c789504ac 100644 --- a/src/calloverlaymodel.cpp +++ b/src/calloverlaymodel.cpp @@ -88,8 +88,6 @@ PendingConferenceesListModel::data(const QModelIndex& index, int role) const return QVariant(false); } - // Since we are using image provider right now, image url representation should be unique to - // be able to use the image cache, account avatar will only be updated once PictureUid changed switch (role) { case Role::PrimaryName: return QVariant(contactModel->bestNameForContact(pendingConferenceeContactUri)); diff --git a/src/commoncomponents/AccountMigrationDialog.qml b/src/commoncomponents/AccountMigrationDialog.qml index 536257de1..67ae6920b 100644 --- a/src/commoncomponents/AccountMigrationDialog.qml +++ b/src/commoncomponents/AccountMigrationDialog.qml @@ -285,25 +285,12 @@ Window { anchors.fill: parent color: "transparent" - AvatarImage { + Avatar { id: avatarImg anchors.fill: parent - showPresenceIndicator: false - - fillMode: Image.PreserveAspectCrop - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: avatarImg.width - height: avatarImg.height - radius: { - var size = ((avatarImg.width <= avatarImg.height)? avatarImg.width:avatarImg.height) - return size / 2 - } - } - } + mode: Avatar.Mode.Account } } } diff --git a/src/commoncomponents/Avatar.qml b/src/commoncomponents/Avatar.qml new file mode 100644 index 000000000..bb6662b75 --- /dev/null +++ b/src/commoncomponents/Avatar.qml @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2020-2021 by Savoir-faire Linux + * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> + * Author: Andreas Traczyk <andreas.traczyk@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 2.14 +import QtQuick.Controls 2.14 + +import net.jami.Adapters 1.0 +import net.jami.Constants 1.0 +import net.jami.Models 1.0 + +Item { + id: root + + enum Mode { Account, Contact, Conversation } + property int mode: Avatar.Mode.Account + + property string imageId + + readonly property string divider: '_' + readonly property string baseProviderPrefix: 'image://avatarImage' + property string typePrefix: { + switch (mode) { + case Avatar.Mode.Account: return 'account' + case Avatar.Mode.Contact: return 'contact' + case Avatar.Mode.Conversation: return 'conversation' + } + } + + property alias presenceStatus: presenceIndicator.status + property bool showPresenceIndicator: true + property alias fillMode: image.fillMode + + onImageIdChanged: image.updateSource() + + Connections { + target: AvatarRegistry + + function onAvatarUidChanged(id) { + // filter this id only + if (id !== root.imageId) + return + + // get the updated uid forcing a new requestImage + // call to the image provider + image.updateSource() + } + } + + Connections { + target: ScreenInfo + + function onDevicePixelRatioChanged() { + image.updateSource() + } + } + + Image { + id: image + + anchors.fill: root + + sourceSize.width: Math.max(24, width) + sourceSize.height: Math.max(24, height) + + smooth: true + antialiasing: true + asynchronous: false + + fillMode: Image.PreserveAspectFit + + function updateSource() { + if (!imageId) + return + source = baseProviderPrefix + '/' + + typePrefix + divider + + imageId + divider + AvatarRegistry.getUid(imageId) + } + + opacity: status === Image.Ready + scale: Math.min(opacity + 0.5, 1.0) + + Behavior on opacity { + NumberAnimation { + from: 0 + duration: JamiTheme.shortFadeDuration + } + } + } + + PresenceIndicator { + id: presenceIndicator + + anchors.right: root.right + anchors.rightMargin: -1 + anchors.bottom: root.bottom + anchors.bottomMargin: -1 + + size: root.width * JamiTheme.avatarPresenceRatio + + visible: showPresenceIndicator + } +} diff --git a/src/commoncomponents/AvatarImage.qml b/src/commoncomponents/AvatarImage.qml deleted file mode 100644 index b10a8bb63..000000000 --- a/src/commoncomponents/AvatarImage.qml +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright (C) 2020 by Savoir-faire Linux - * Author: Mingrui Zhang <mingrui.zhang@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 2.14 -import QtQuick.Controls 2.14 - -import net.jami.Adapters 1.0 -import net.jami.Constants 1.0 -import net.jami.Models 1.0 - -SpinningAnimation { - id: root - - enum AvatarMode { - FromAccount = 0, - FromFile, - FromContactUri, - FromConvUid, - FromBase64, - FromTemporaryName, - Default - } - - property alias fillMode: rootImage.fillMode - property alias sourceSize: rootImage.sourceSize - property int transitionDuration: 150 - property bool saveToConfig: false - property int avatarMode: AvatarImage.AvatarMode.FromAccount - property string imageProviderIdPrefix: { - switch (avatarMode) { - case AvatarImage.AvatarMode.FromAccount: - return "account_" - case AvatarImage.AvatarMode.FromFile: - return "file_" - case AvatarImage.AvatarMode.FromContactUri: - return "contact_" - case AvatarImage.AvatarMode.FromConvUid: - return "conversation_" - case AvatarImage.AvatarMode.FromTemporaryName: - return "fallback_" - case AvatarImage.AvatarMode.FromBase64: - return "base64_" - case AvatarImage.AvatarMode.Default: - return "default_" - default: - return "" - } - } - - // Full request url example: forceUpdateUrl_xxxxxxx_account_xxxxxxxx - property string imageProviderUrl: "image://avatarImage/" + forceUpdateUrl - + "_" + imageProviderIdPrefix - property string imageId: "" - property string forceUpdateUrl: Date.now() - property alias presenceStatus: presenceIndicator.status - property bool showPresenceIndicator: true - property int unreadMessagesCount: 0 - property bool enableFadeAnimation: true - - signal imageIsReady - - function saveAvatarToConfig() { - switch (avatarMode) { - case AvatarImage.AvatarMode.FromFile: - AccountAdapter.setCurrAccAvatar(true, imageId) - break - case AvatarImage.AvatarMode.FromBase64: - AccountAdapter.setCurrAccAvatar(false, imageId) - break - default: - return - } - } - - function updateImage(updatedId, oneTimeForceUpdateUrl) { - imageId = updatedId - if (oneTimeForceUpdateUrl === undefined) - forceUpdateUrl = Date.now() - else - forceUpdateUrl = oneTimeForceUpdateUrl - - rootImage.source = imageProviderUrl + imageId - - if (saveToConfig) - saveAvatarToConfig() - } - - function reloadImageSource() { - var tempEnableAnimation = enableFadeAnimation - var tempImageSource = rootImage.source - - enableFadeAnimation = false - rootImage.source = "" - - enableFadeAnimation = tempEnableAnimation - rootImage.source = tempImageSource - } - - function rootImageOverlayReadyCallback() { - if (rootImageOverlay.status === Image.Ready - && (rootImageOverlay.state === "avatarImgFadeIn")) { - rootImageOverlay.statusChanged.disconnect( - rootImageOverlayReadyCallback) - - rootImageOverlay.state = '' - } - } - - Item { - id: imageGroup - - anchors.centerIn: root - - width: root.width - spinningAnimationWidth - height: root.height - spinningAnimationWidth - - Image { - id: rootImage - - anchors.fill: imageGroup - - smooth: true - antialiasing: true - asynchronous: true - - sourceSize.width: Math.max(24, width) - sourceSize.height: Math.max(24, height) - - fillMode: Image.PreserveAspectFit - - onStatusChanged: { - if (status === Image.Ready) { - if (enableFadeAnimation) { - rootImageOverlay.state = "avatarImgFadeIn" - } else { - rootImageOverlay.source = rootImage.source - root.imageIsReady() - } - } - } - - Component.onCompleted: { - if (imageId) - return source = imageProviderUrl + imageId - return source = "" - } - - Image { - id: rootImageOverlay - - anchors.fill: rootImage - - smooth: true - antialiasing: true - asynchronous: true - - sourceSize.width: Math.max(24, width) - sourceSize.height: Math.max(24, height) - - fillMode: Image.PreserveAspectFit - - states: State { - name: "avatarImgFadeIn" - PropertyChanges { - target: rootImageOverlay - opacity: 0 - } - } - - transitions: Transition { - NumberAnimation { - properties: "opacity" - easing.type: Easing.InOutQuad - duration: enableFadeAnimation ? 400 : 0 - } - - onRunningChanged: { - if ((rootImageOverlay.state === "avatarImgFadeIn") - && (!running)) { - if (rootImageOverlay.source === rootImage.source) { - rootImageOverlay.state = '' - return - } - rootImageOverlay.statusChanged.connect( - rootImageOverlayReadyCallback) - rootImageOverlay.source = rootImage.source - } - } - } - } - } - - PresenceIndicator { - id: presenceIndicator - - anchors.right: imageGroup.right - anchors.rightMargin: -1 - anchors.bottom: imageGroup.bottom - anchors.bottomMargin: -1 - - size: imageGroup.width * 0.26 - - visible: showPresenceIndicator - } - - Connections { - target: ScreenInfo - - function onDevicePixelRatioChanged() { - reloadImageSource() - } - } - } -} diff --git a/src/commoncomponents/PhotoboothView.qml b/src/commoncomponents/PhotoboothView.qml index 4ff0e989b..c8ca3c611 100644 --- a/src/commoncomponents/PhotoboothView.qml +++ b/src/commoncomponents/PhotoboothView.qml @@ -27,199 +27,113 @@ import net.jami.Adapters 1.0 import net.jami.Constants 1.0 ColumnLayout { - property int photoState: PhotoboothView.PhotoState.Default - property bool avatarSet: false - // saveToConfig is to specify whether the image should be saved to account config - property alias saveToConfig: avatarImg.saveToConfig - property string fileName: "" - - property int boothWidth: 224 - - enum PhotoState { - Default = 0, - CameraRendering, - Taken - } + id: root - readonly property int size: boothWidth + - buttonsRowLayout.height + - JamiTheme.preferredMarginSize / 2 + enum Mode { Static, Previewing } + property int mode: PhotoboothView.Mode.Static + property alias imageId: avatar.imageId - function initUI(useDefaultAvatar = true) { - photoState = PhotoboothView.PhotoState.Default - avatarSet = false - if (useDefaultAvatar) - setAvatarImage(AvatarImage.AvatarMode.Default, "") - } + property int size: 224 function startBooth() { AccountAdapter.startPreviewing(false) - photoState = PhotoboothView.PhotoState.CameraRendering + mode = PhotoboothView.Mode.Previewing } function stopBooth(){ - try{ - if(!AccountAdapter.hasVideoCall()) { - AccountAdapter.stopPreviewing() - } - } catch(erro){console.log("Exception: " + erro.message)} - } - - function setAvatarImage(mode = AvatarImage.AvatarMode.FromAccount, - imageId = LRCInstance.currentAccountId){ - if (mode !== AvatarImage.AvatarMode.FromBase64) - avatarImg.enableFadeAnimation = true - else - avatarImg.enableFadeAnimation = false - - avatarImg.avatarMode = mode - - if (mode === AvatarImage.AvatarMode.Default) { - avatarImg.updateImage(imageId) - return + if (!AccountAdapter.hasVideoCall()) { + AccountAdapter.stopPreviewing() } - - if (imageId) - avatarImg.updateImage(imageId) - } - - function manualSaveToConfig() { - avatarImg.saveAvatarToConfig() + mode = PhotoboothView.Mode.Static } onVisibleChanged: { - if(!visible){ + if (visible) { + mode = PhotoboothView.Mode.Static + } else { stopBooth() } } spacing: 0 - JamiFileDialog{ - id: importFromFileToAvatar_Dialog + JamiFileDialog { + id: importFromFileDialog mode: JamiFileDialog.OpenFile title: JamiStrings.chooseAvatarImage folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation) - nameFilters: [ qsTr("Image Files") + " (*.png *.jpg *.jpeg)",qsTr( - "All files") + " (*)"] + nameFilters: [ + qsTr("Image Files") + " (*.png *.jpg *.jpeg)", + qsTr("All files") + " (*)" + ] onAccepted: { - avatarSet = true - photoState = PhotoboothView.PhotoState.Default - - fileName = file - if (fileName.length === 0) { - SettingsAdapter.clearCurrentAvatar() - setAvatarImage() - return - } - - setAvatarImage(AvatarImage.AvatarMode.FromFile, - UtilsAdapter.getAbsPath(fileName)) + var filePath = UtilsAdapter.getAbsPath(file) + AccountAdapter.setCurrentAccountAvatarFile(filePath) } } - Label { - id: avatarLabel - - visible: photoState !== PhotoboothView.PhotoState.CameraRendering + Item { + id: imageLayer - Layout.fillWidth: true - Layout.maximumWidth: boothWidth - Layout.preferredHeight: boothWidth + Layout.preferredWidth: size + Layout.preferredHeight: size Layout.alignment: Qt.AlignHCenter - background: Rectangle { - id: avatarLabelBackground + Avatar { + id: avatar anchors.fill: parent - color: "white" - radius: height / 2 - - AvatarImage { - id: avatarImg - - anchors.centerIn: avatarLabelBackground - width: avatarLabelBackground.width + avatarImg.spinningAnimationWidth - height: avatarLabelBackground.height + avatarImg.spinningAnimationWidth - - showPresenceIndicator: false - - fillMode: Image.PreserveAspectCrop - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: avatarImg.width - height: avatarImg.height - radius: { - var size = ((avatarImg.width <= avatarImg.height) ? - avatarImg.width:avatarImg.height) - return size / 2 - } - } - } + anchors.margins: 1 - onImageIsReady: { - if (avatarMode === AvatarImage.AvatarMode.FromBase64) - photoState = PhotoboothView.PhotoState.Taken + visible: !preview.visible - if (photoState === PhotoboothView.PhotoState.Taken) { - avatarImg.state = "" - avatarImg.state = "flashIn" - } - } + fillMode: Image.PreserveAspectCrop + showPresenceIndicator: false + } - onOpacityChanged: { - if (avatarImg.state === "flashIn" && opacity === 0) - avatarImg.state = "flashOut" - } + PhotoboothPreviewRender { + id: preview + + anchors.fill: parent + anchors.margins: 1 + + visible: mode === PhotoboothView.Mode.Previewing + + onRenderingStopped: stopBooth() + lrcInstance: LRCInstance - states: [ - State { - name: "flashIn" - PropertyChanges { target: avatarImg; opacity: 0} - }, State { - name: "flashOut" - PropertyChanges { target: avatarImg; opacity: 1} - }] - - transitions: Transition { - NumberAnimation { - properties: "opacity" - easing.type: Easing.Linear - duration: 100 - } + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: size + height: size + radius: size / 2 } } } - } - PhotoboothPreviewRender { - id:previewWidget + Rectangle { + id: flashRect - onHideBooth: stopBooth() + anchors.fill: parent + anchors.margins: 0 + radius: size / 2 + color: "white" + opacity: 0 - visible: photoState === PhotoboothView.PhotoState.CameraRendering - focus: visible + SequentialAnimation { + id: flashAnimation - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: boothWidth - Layout.preferredHeight: boothWidth - - lrcInstance: LRCInstance - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: previewWidget.width - height: previewWidget.height - radius: { - var size = ((previewWidget.width <= previewWidget.height) ? - previewWidget.width:previewWidget.height) - return size / 2 + NumberAnimation { + target: flashRect; property: "opacity" + to: 1; duration: 0 + } + NumberAnimation { + target: flashRect; property: "opacity" + to: 0; duration: 500 } } } @@ -229,50 +143,33 @@ ColumnLayout { id: buttonsRowLayout Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter Layout.preferredHeight: JamiTheme.preferredFieldHeight Layout.topMargin: JamiTheme.preferredMarginSize / 2 + Layout.alignment: Qt.AlignHCenter PushButton { id: takePhotoButton - property string cameraAltIconUrl: "qrc:/images/icons/baseline-camera_alt-24px.svg" - property string addPhotoIconUrl: "qrc:/images/icons/round-add_a_photo-24px.svg" - property string refreshIconUrl: "qrc:/images/icons/baseline-refresh-24px.svg" - Layout.alignment: Qt.AlignHCenter + radius: JamiTheme.primaryRadius imageColor: JamiTheme.textColor - toolTipText: JamiStrings.takePhoto - radius: height / 6 - source: { - if(photoState === PhotoboothView.PhotoState.Default) { - toolTipText = qsTr("Take photo") - return cameraAltIconUrl - } - - if(photoState === PhotoboothView.PhotoState.Taken){ - toolTipText = qsTr("Retake photo") - return refreshIconUrl - } else { - toolTipText = qsTr("Take photo") - return addPhotoIconUrl - } - } + source: mode === PhotoboothView.Mode.Static ? + "qrc:/images/icons/baseline-camera_alt-24px.svg" : + "qrc:/images/icons/round-add_a_photo-24px.svg" onClicked: { - if(photoState !== PhotoboothView.PhotoState.CameraRendering){ - startBooth() - return - } else { - setAvatarImage(AvatarImage.AvatarMode.FromBase64, - previewWidget.takePhoto(boothWidth)) - - avatarSet = true + if (mode === PhotoboothView.Mode.Previewing) { + flashAnimation.start() + AccountAdapter.setCurrentAccountAvatarBase64( + preview.takePhoto(size)) stopBooth() + return } + + startBooth() } } @@ -283,14 +180,15 @@ ColumnLayout { Layout.preferredHeight: JamiTheme.preferredFieldHeight Layout.alignment: Qt.AlignHCenter - radius: height / 6 + radius: JamiTheme.primaryRadius source: "qrc:/images/icons/round-folder-24px.svg" toolTipText: JamiStrings.importFromFile imageColor: JamiTheme.textColor onClicked: { - importFromFileToAvatar_Dialog.open() + stopBooth() + importFromFileDialog.open() } } } diff --git a/src/commoncomponents/SpinningAnimation.qml b/src/commoncomponents/SpinningAnimation.qml index f8061b23f..d0edd4c14 100644 --- a/src/commoncomponents/SpinningAnimation.qml +++ b/src/commoncomponents/SpinningAnimation.qml @@ -19,21 +19,19 @@ import QtQuick 2.14 import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.14 -import QtQuick.Controls.Universal 2.14 import QtGraphicalEffects 1.12 Item { id: root - enum SpinningAnimationMode { - DISABLED = 0, - NORMAL, - SYMMETRY + enum Mode { + Disabled, + Radial, + BiRadial } - property int spinningAnimationMode: SpinningAnimation.SpinningAnimationMode.DISABLED - property int spinningAnimationWidth: 5 + property int mode: SpinningAnimation.Mode.Disabled + property int spinningAnimationWidth: 4 property real outerCutRadius: root.height / 2 property int spinningAnimationDuration: 1000 @@ -42,7 +40,7 @@ Item { anchors.fill: parent - visible: spinningAnimationMode !== SpinningAnimation.SpinningAnimationMode.DISABLED + visible: mode !== SpinningAnimation.Mode.Disabled angle: 0.0 gradient: Gradient { GradientStop { position: 0.5; color: "transparent" } @@ -77,7 +75,7 @@ Item { anchors.fill: parent - visible: spinningAnimationMode === SpinningAnimation.SpinningAnimationMode.SYMMETRY + visible: mode === SpinningAnimation.Mode.BiRadial angle: 180.0 gradient: Gradient { GradientStop { @@ -113,7 +111,7 @@ Item { } } - layer.enabled: spinningAnimationMode !== SpinningAnimation.SpinningAnimationMode.DISABLED + layer.enabled: mode !== SpinningAnimation.Mode.Disabled layer.effect: OpacityMask { maskSource: Rectangle { width: root.width diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml index 391cc32bb..d9356ba1f 100644 --- a/src/constant/JamiStrings.qml +++ b/src/constant/JamiStrings.qml @@ -392,8 +392,7 @@ Item { // PhotoBoothView property string chooseAvatarImage: qsTr("Choose a picture as avatar") property string importFromFile: qsTr("Import avatar from image file") - property string takePhone: qsTr("Take photo") - property string retakePhone: qsTr("Retake photo") + property string takePhoto: qsTr("Take photo") // PluginSettingsPage property string enable: qsTr("Enable") diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml index c3659bd00..687946aa8 100644 --- a/src/constant/JamiTheme.qml +++ b/src/constant/JamiTheme.qml @@ -228,6 +228,7 @@ Item { property int mosaicButtonTextPointSize: 8 property int mosaicButtonPreferredWidth: 70 property int mosaicButtonMaxWidth: 100 + property real avatarPresenceRatio: 0.26 property int menuItemsPreferredWidth: 220 property int menuItemsPreferredHeight: 48 diff --git a/src/conversationlistmodelbase.cpp b/src/conversationlistmodelbase.cpp index f65cff1aa..e43a1d454 100644 --- a/src/conversationlistmodelbase.cpp +++ b/src/conversationlistmodelbase.cpp @@ -171,8 +171,6 @@ ConversationListModelBase::dataForItem(item_t item, int role) const return QVariant(contactModel->bestIdForContact(peerUri)); case Role::Presence: return QVariant(contact.isPresent); - case Role::PictureUid: - return QVariant(contactAvatarUidMap_[peerUri]); case Role::Alias: return QVariant(contact.profileInfo.alias); case Role::RegisteredName: @@ -188,37 +186,3 @@ ConversationListModelBase::dataForItem(item_t item, int role) const return {}; } - -void -ConversationListModelBase::updateContactAvatarUid(const QString& contactUri) -{ - contactAvatarUidMap_[contactUri] = Utils::generateUid(); -} - -void -ConversationListModelBase::fillContactAvatarUidMap( - const lrc::api::ContactModel::ContactInfoMap& contacts) -{ - if (contacts.size() == 0) { - contactAvatarUidMap_.clear(); - return; - } - - if (contactAvatarUidMap_.isEmpty() || contacts.size() != contactAvatarUidMap_.size()) { - bool useContacts = contacts.size() > contactAvatarUidMap_.size(); - auto contactsKeyList = contacts.keys(); - auto contactAvatarUidMapKeyList = contactAvatarUidMap_.keys(); - - for (int i = 0; - i < (useContacts ? contactsKeyList.size() : contactAvatarUidMapKeyList.size()); - ++i) { - // Insert or update - if (i < contactsKeyList.size() && !contactAvatarUidMap_.contains(contactsKeyList.at(i))) - contactAvatarUidMap_.insert(contactsKeyList.at(i), Utils::generateUid()); - // Remove - if (i < contactAvatarUidMapKeyList.size() - && !contacts.contains(contactAvatarUidMapKeyList.at(i))) - contactAvatarUidMap_.remove(contactAvatarUidMapKeyList.at(i)); - } - } -} diff --git a/src/conversationlistmodelbase.h b/src/conversationlistmodelbase.h index 40fc0fcd7..954baca02 100644 --- a/src/conversationlistmodelbase.h +++ b/src/conversationlistmodelbase.h @@ -43,7 +43,6 @@ X(CallState) \ X(SectionName) \ X(AccountId) \ - X(PictureUid) \ X(Draft) \ X(IsRequest) \ X(Mode) \ @@ -76,18 +75,9 @@ public: QVariant dataForItem(item_t item, int role = Qt::DisplayRole) const; - // Update the avatar uid map to prevent the image provider from pulling from the cache - void updateContactAvatarUid(const QString& contactUri); - protected: using Role = ConversationList::Role; - // Assign a uid for each contact avatar; it will serve as the PictureUid role - void fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts); - // Convenience pointer to be pulled from lrcinstance ConversationModel* model_; - - // AvatarImageProvider helper - QMap<QString, QString> contactAvatarUidMap_; }; diff --git a/src/conversationsadapter.cpp b/src/conversationsadapter.cpp index 836e0daa6..1466a1697 100644 --- a/src/conversationsadapter.cpp +++ b/src/conversationsadapter.cpp @@ -211,7 +211,9 @@ ConversationsAdapter::onNewReadInteraction(const QString& accountId, } void -ConversationsAdapter::onNewTrustRequest(const QString& accountId, const QString& convId, const QString& peerUri) +ConversationsAdapter::onNewTrustRequest(const QString& accountId, + const QString& convId, + const QString& peerUri) { #ifdef Q_OS_LINUX if (!QApplication::focusWindow() || accountId != lrcInstance_->get_currentAccountId()) { @@ -262,10 +264,10 @@ ConversationsAdapter::onProfileUpdated(const QString& contactUri) auto& convInfo = lrcInstance_->getConversationFromPeerUri(contactUri); if (convInfo.uid.isEmpty()) return; + + // notify UI elements auto row = lrcInstance_->indexOf(convInfo.uid); const auto index = convSrcModel_->index(row, 0); - - convSrcModel_->updateContactAvatarUid(contactUri); Q_EMIT convSrcModel_->dataChanged(index, index); } @@ -416,7 +418,7 @@ ConversationsAdapter::connectConversationModel() &ConversationsAdapter::onModelChanged, Qt::UniqueConnection); - QObject::connect(lrcInstance_->getCurrentAccountInfo().contactModel.get(), + QObject::connect(lrcInstance_->getCurrentContactModel(), &ContactModel::profileUpdated, this, &ConversationsAdapter::onProfileUpdated, diff --git a/src/lrcinstance.cpp b/src/lrcinstance.cpp index 5d2adb183..8f1060613 100644 --- a/src/lrcinstance.cpp +++ b/src/lrcinstance.cpp @@ -263,34 +263,6 @@ LRCInstance::getCurrentAccountIndex() return -1; } -void -LRCInstance::setAvatarForAccount(const QPixmap& avatarPixmap, const QString& accountID) -{ - QByteArray ba; - QBuffer bu(&ba); - bu.open(QIODevice::WriteOnly); - avatarPixmap.save(&bu, "PNG"); - auto str = QString::fromLocal8Bit(ba.toBase64()); - accountModel().setAvatar(accountID, str); -} - -void -LRCInstance::setCurrAccAvatar(const QPixmap& avatarPixmap) -{ - QByteArray ba; - QBuffer bu(&ba); - bu.open(QIODevice::WriteOnly); - avatarPixmap.save(&bu, "PNG"); - auto str = QString::fromLocal8Bit(ba.toBase64()); - accountModel().setAvatar(get_currentAccountId(), str); -} - -void -LRCInstance::setCurrAccAvatar(const QString& avatar) -{ - accountModel().setAvatar(get_currentAccountId(), avatar); -} - void LRCInstance::setCurrAccDisplayName(const QString& displayName) { diff --git a/src/lrcinstance.h b/src/lrcinstance.h index 5c1082d03..7e572ab9f 100644 --- a/src/lrcinstance.h +++ b/src/lrcinstance.h @@ -107,9 +107,6 @@ public: const QString& content); int getCurrentAccountIndex(); - void setAvatarForAccount(const QPixmap& avatarPixmap, const QString& accountID); - void setCurrAccAvatar(const QPixmap& avatarPixmap); - void setCurrAccAvatar(const QString& avatar); void setCurrAccDisplayName(const QString& displayName); const account::ConfProperties_t& getCurrAccConfig(); int indexOf(const QString& convId); diff --git a/src/mainapplication.cpp b/src/mainapplication.cpp index 7426fc4e9..2e07c911b 100644 --- a/src/mainapplication.cpp +++ b/src/mainapplication.cpp @@ -29,6 +29,7 @@ #include "qrimageprovider.h" #include "tintedbuttonimageprovider.h" #include "avatarimageprovider.h" +#include "avatarregistry.h" #include "accountadapter.h" #include "avadapter.h" @@ -450,6 +451,9 @@ MainApplication::initQmlLayer() QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, settingsAdapter, "SettingsAdapter"); QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, pluginAdapter, "PluginAdapter"); + auto avatarRegistry = new AvatarRegistry(lrcInstance_.data(), this); + QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, avatarRegistry, "AvatarRegistry"); + // TODO: remove these QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, AVModel, &lrcInstance_->avModel()) QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, PluginModel, &lrcInstance_->pluginModel()) diff --git a/src/mainview/MainView.qml b/src/mainview/MainView.qml index 97d1bc058..aeb1886b1 100644 --- a/src/mainview/MainView.qml +++ b/src/mainview/MainView.qml @@ -279,14 +279,6 @@ Rectangle { visible: (mainViewSidePanel.visible || settingsMenu.visible) - Connections { - target: AccountAdapter - - function onAccountStatusChanged(accountId) { - accountComboBox.resetAccountListModel(accountId) - } - } - onSettingBtnClicked: { toggleSettingsView() } diff --git a/src/mainview/components/AccountComboBox.qml b/src/mainview/components/AccountComboBox.qml index 4e7a32e12..54faf20d6 100644 --- a/src/mainview/components/AccountComboBox.qml +++ b/src/mainview/components/AccountComboBox.qml @@ -39,7 +39,7 @@ Label { target: AccountAdapter function onAccountStatusChanged(accountId) { - resetAccountListModel(accountId) + AccountListModel.reset() } } @@ -48,15 +48,10 @@ Label { function onAccountListChanged() { root.update() - resetAccountListModel(LRCInstance.currentAccountId) + AccountListModel.reset() } } - function resetAccountListModel(accountId) { - AccountListModel.updateAvatarUid(accountId) - AccountListModel.reset() - } - function togglePopup() { if (root.popup.opened) { root.popup.close() @@ -112,9 +107,7 @@ Label { target: AccountListModel function onModelReset() { - avatar.updateImage(LRCInstance.currentAccountId, - AccountListModel.data(AccountListModel.index(0, 0), - AccountList.PictureUid)) + avatar.imageId = LRCInstance.currentAccountId avatar.presenceStatus = AccountListModel.data(AccountListModel.index(0, 0), AccountList.Status) userAliasText.text = AccountListModel.data(AccountListModel.index(0,0), @@ -130,7 +123,7 @@ Label { anchors.rightMargin: 15 spacing: 10 - AvatarImage { + Avatar { id: avatar Layout.preferredWidth: JamiTheme.accountListAvatarSize @@ -138,9 +131,7 @@ Label { Layout.alignment: Qt.AlignVCenter imageId: LRCInstance.currentAccountId - - presenceStatus: AccountListModel.data(AccountListModel.index(0, 0), - AccountList.Status) + mode: Avatar.Mode.Account } ColumnLayout { diff --git a/src/mainview/components/AccountItemDelegate.qml b/src/mainview/components/AccountItemDelegate.qml index 13dcca576..4343bcef4 100644 --- a/src/mainview/components/AccountItemDelegate.qml +++ b/src/mainview/components/AccountItemDelegate.qml @@ -49,14 +49,15 @@ ItemDelegate { anchors.rightMargin: 15 spacing: 10 - AvatarImage { + Avatar { Layout.preferredWidth: JamiTheme.accountListAvatarSize Layout.preferredHeight: JamiTheme.accountListAvatarSize Layout.alignment: Qt.AlignVCenter presenceStatus: Status - Component.onCompleted: updateImage(ID, PictureUid) + imageId: ID + mode: Avatar.Mode.Account } ColumnLayout { diff --git a/src/mainview/components/ContactPickerItemDelegate.qml b/src/mainview/components/ContactPickerItemDelegate.qml index 0714f7f48..18673b71c 100644 --- a/src/mainview/components/ContactPickerItemDelegate.qml +++ b/src/mainview/components/ContactPickerItemDelegate.qml @@ -29,10 +29,10 @@ import "../../commoncomponents" ItemDelegate { id: contactPickerItemDelegate - property alias showPresenceIndicator: contactPickerContactImage.showPresenceIndicator + property alias showPresenceIndicator: avatar.showPresenceIndicator - AvatarImage { - id: contactPickerContactImage + ConversationAvatar { + id: avatar anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter @@ -41,18 +41,17 @@ ItemDelegate { width: 40 height: 40 - avatarMode: AvatarImage.AvatarMode.FromContactUri - imageId: URI + imageId: UID } Rectangle { id: contactPickerContactInfoRect - anchors.left: contactPickerContactImage.right + anchors.left: avatar.right anchors.leftMargin: 10 anchors.top: parent.top - width: parent.width - contactPickerContactImage.width - 20 + width: parent.width - avatar.width - 20 height: parent.height color: "transparent" @@ -108,7 +107,7 @@ ItemDelegate { implicitHeight: Math.max( contactPickerContactName.height + textMetricsContactPickerContactId.height + 10, - contactPickerContactImage.height + 10) + avatar.height + 10) border.width: 0 } diff --git a/src/mainview/components/ConversationAvatar.qml b/src/mainview/components/ConversationAvatar.qml new file mode 100644 index 000000000..f47d0d231 --- /dev/null +++ b/src/mainview/components/ConversationAvatar.qml @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2021 by Savoir-faire Linux + * Author: Andreas Traczyk <andreas.traczyk@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 2.14 +import QtQuick.Controls 2.14 + +import net.jami.Adapters 1.0 + +import "../../commoncomponents" + +Item { + id: root + + property alias imageId: avatar.imageId + property alias showPresenceIndicator: avatar.showPresenceIndicator + property alias animationMode: animation.mode + + SpinningAnimation { + id: animation + + anchors.fill: root + } + + Avatar { + id: avatar + + anchors.fill: root + anchors.margins: animation.mode === SpinningAnimation.Mode.Disabled ? + 0 : + animation.spinningAnimationWidth + + mode: Avatar.Mode.Conversation + } +} diff --git a/src/mainview/components/ConversationListView.qml b/src/mainview/components/ConversationListView.qml index b74701b40..91cf6f546 100644 --- a/src/mainview/components/ConversationListView.qml +++ b/src/mainview/components/ConversationListView.qml @@ -146,7 +146,7 @@ ListView { userProfile.aliasText = item.displayName userProfile.registeredNameText = item.displayId userProfile.idText = item.uri - userProfile.contactImageUid = item.convId + userProfile.convId = item.convId userProfile.isSwarm = item.isSwarm openMenu() diff --git a/src/mainview/components/InitialCallPage.qml b/src/mainview/components/InitialCallPage.qml index d09e4a0a0..0e67602e4 100644 --- a/src/mainview/components/InitialCallPage.qml +++ b/src/mainview/components/InitialCallPage.qml @@ -56,7 +56,7 @@ Rectangle { onAccountConvPairChanged: { if (accountConvPair[1]) { - contactImg.updateImage(accountConvPair[1]) + contactImg.imageId = accountConvPair[1] root.bestName = UtilsAdapter.getBestName(accountConvPair[0], accountConvPair[1]) } } @@ -73,16 +73,15 @@ Rectangle { anchors.horizontalCenter: root.horizontalCenter anchors.verticalCenter: root.verticalCenter - AvatarImage { + ConversationAvatar { id: contactImg Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: JamiTheme.avatarSizeInCall + spinningAnimationWidth - Layout.preferredHeight: JamiTheme.avatarSizeInCall + spinningAnimationWidth + Layout.preferredWidth: JamiTheme.avatarSizeInCall + Layout.preferredHeight: JamiTheme.avatarSizeInCall - avatarMode: AvatarImage.AvatarMode.FromConvUid showPresenceIndicator: false - spinningAnimationMode: SpinningAnimation.SpinningAnimationMode.NORMAL + animationMode: SpinningAnimation.Mode.Radial } Text { diff --git a/src/mainview/components/OngoingCallPage.qml b/src/mainview/components/OngoingCallPage.qml index e037c9b2a..b7f0b0cc6 100644 --- a/src/mainview/components/OngoingCallPage.qml +++ b/src/mainview/components/OngoingCallPage.qml @@ -51,7 +51,7 @@ Rectangle { onAccountPeerPairChanged: { if (accountPeerPair[0] === "" || accountPeerPair[1] === "") return - contactImage.updateImage(accountPeerPair[1]) + contactImage.imageId = accountPeerPair[1] callOverlay.participantsLayer.update(CallAdapter.getConferencesInfos()) bestName = UtilsAdapter.getBestName(accountPeerPair[0], @@ -363,14 +363,13 @@ Rectangle { visible: root.isAudioOnly - AvatarImage { + ConversationAvatar { id: contactImage Layout.alignment: Qt.AlignCenter Layout.preferredWidth: JamiTheme.avatarSizeInCall Layout.preferredHeight: JamiTheme.avatarSizeInCall - avatarMode: AvatarImage.AvatarMode.FromConvUid showPresenceIndicator: false } diff --git a/src/mainview/components/ParticipantCallInStatusDelegate.qml b/src/mainview/components/ParticipantCallInStatusDelegate.qml index b21219601..965a6ae7b 100644 --- a/src/mainview/components/ParticipantCallInStatusDelegate.qml +++ b/src/mainview/components/ParticipantCallInStatusDelegate.qml @@ -32,7 +32,7 @@ SpinningAnimation { width: contentRect.width + spinningAnimationWidth height: JamiTheme.participantCallInStatusDelegateHeight - spinningAnimationMode: SpinningAnimation.SpinningAnimationMode.SYMMETRY + mode: SpinningAnimation.Mode.BiRadial outerCutRadius: JamiTheme.participantCallInStatusDelegateRadius spinningAnimationDuration: 5000 @@ -49,7 +49,7 @@ SpinningAnimation { opacity: JamiTheme.participantCallInStatusOpacity radius: JamiTheme.participantCallInStatusDelegateRadius - AvatarImage { + Avatar { id: avatar anchors.left: contentRect.left @@ -60,7 +60,7 @@ SpinningAnimation { height: JamiTheme.participantCallInAvatarSize showPresenceIndicator: false - avatarMode: AvatarImage.AvatarMode.FromContactUri + mode: Avatar.Mode.Contact imageId: ContactUri } diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml index 7aa24d2c1..dc7aad32e 100644 --- a/src/mainview/components/ParticipantOverlay.qml +++ b/src/mainview/components/ParticipantOverlay.qml @@ -52,16 +52,12 @@ Item { z: 1 - function setAvatar(show, avatar, uri, local, isContact) { + function setAvatar(show, base64, uri, local, isContact) { if (!show) contactImage.visible = false else { - if (avatar) { - contactImage.avatarMode = AvatarImage.AvatarMode.FromBase64 - contactImage.updateImage(avatar) - } else if (local) { - contactImage.avatarMode = AvatarImage.AvatarMode.FromAccount - contactImage.updateImage(LRCInstance.currentAccountId) + if (local) { + contactImage.imageId = LRCInstance.currentAccountId } else if (isContact) { contactImage.avatarMode = AvatarImage.AvatarMode.FromContactUri contactImage.updateImage(uri) @@ -173,33 +169,15 @@ Item { } } - AvatarImage { + ConversationAvatar { id: contactImage anchors.centerIn: parent height: Math.min(parent.width / 2, parent.height / 2) width: Math.min(parent.width / 2, parent.height / 2) - fillMode: Image.PreserveAspectFit - imageId: "" visible: false - avatarMode: AvatarImage.AvatarMode.Default showPresenceIndicator: false - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: contactImage.width - height: contactImage.height - radius: { - var size = ((contactImage.width <= contactImage.height)? - contactImage.width : contactImage.height) - return size / 2 - } - } - } - layer.mipmap: false - layer.smooth: true } // Participant background and buttons for moderation diff --git a/src/mainview/components/SmartListItemDelegate.qml b/src/mainview/components/SmartListItemDelegate.qml index ab24f1beb..3afc6d6b1 100644 --- a/src/mainview/components/SmartListItemDelegate.qml +++ b/src/mainview/components/SmartListItemDelegate.qml @@ -37,45 +37,27 @@ ItemDelegate { return UID } - Component.onCompleted: { - if (ContactType === Profile.Type.TEMPORARY) - root.ListView.view.model.updateContactAvatarUid(URI) - avatar.updateImage(URI, PictureUid) - } - RowLayout { anchors.fill: parent anchors.leftMargin: 15 anchors.rightMargin: 15 spacing: 10 - AvatarImage { + ConversationAvatar { id: avatar - Connections { - target: root.ListView.view.model - function onDataChanged(idx) { - // TODO: currently the avatar dispaly mechanism requires - // that each dataChanged signal is caught by and induces an - // updateImage call per smartlist item. Once this is fixed - // we can filter for the current delegate's index like: - // if (idx.row !== index) return - avatar.updateImage(URI, PictureUid) - } - } + imageId: UID + showPresenceIndicator: Presence Layout.preferredWidth: JamiTheme.smartListAvatarSize Layout.preferredHeight: JamiTheme.smartListAvatarSize - - avatarMode: AvatarImage.AvatarMode.FromContactUri - showPresenceIndicator: Presence === undefined ? false : Presence - transitionDuration: 0 } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: 0 + // best name Text { Layout.fillWidth: true @@ -93,6 +75,7 @@ ItemDelegate { Layout.fillWidth: true Layout.preferredHeight: 20 Layout.alignment: Qt.AlignTop + // last Interaction date Text { Layout.alignment: Qt.AlignVCenter @@ -101,6 +84,7 @@ ItemDelegate { font.weight: UnreadMessagesCount ? Font.DemiBold : Font.Normal color: JamiTheme.textColor } + // last Interaction Text { elide: Text.ElideRight @@ -128,6 +112,7 @@ ItemDelegate { Layout.preferredWidth: childrenRect.width Layout.fillHeight: true spacing: 2 + // call status Text { Layout.preferredHeight: 20 @@ -137,6 +122,7 @@ ItemDelegate { font.weight: Font.Medium color: JamiTheme.textColor } + // unread message count Item { Layout.preferredWidth: childrenRect.width diff --git a/src/mainview/components/UserProfile.qml b/src/mainview/components/UserProfile.qml index 1c275bf62..4557fd99c 100644 --- a/src/mainview/components/UserProfile.qml +++ b/src/mainview/components/UserProfile.qml @@ -29,7 +29,7 @@ BaseDialog { id: root property string responsibleConvUid: "" - property string contactImageUid: "" + property string convId: "" property string aliasText: "" property string registeredNameText: "" property string idText: "" @@ -57,17 +57,14 @@ BaseDialog { rowSpacing: 16 columnSpacing: 24 - AvatarImage { + ConversationAvatar { id: contactImage Layout.alignment: Qt.AlignRight Layout.preferredWidth: preferredImgSize Layout.preferredHeight: preferredImgSize - sourceSize.width: preferredImgSize - sourceSize.height: preferredImgSize - - avatarMode: AvatarImage.AvatarMode.FromConvUid + imageId: convId showPresenceIndicator: false } @@ -246,6 +243,4 @@ BaseDialog { if (responsibleConvUid !== "") contactQrImage.source = "image://qrImage/contact_" + responsibleConvUid } - - onContactImageUidChanged: contactImage.updateImage(contactImageUid) } diff --git a/src/previewrenderer.cpp b/src/previewrenderer.cpp index 7235c923c..e692b3537 100644 --- a/src/previewrenderer.cpp +++ b/src/previewrenderer.cpp @@ -122,17 +122,14 @@ PhotoboothPreviewRender::PhotoboothPreviewRender(QQuickItem* parent) { connect(this, &PreviewRenderer::lrcInstanceChanged, [this] { if (lrcInstance_) - rendererStoppedConnection_ = connect(lrcInstance_->renderer(), - &RenderManager::previewRenderingStopped, - [this]() { Q_EMIT hideBooth(); }); + connect(lrcInstance_->renderer(), + &RenderManager::previewRenderingStopped, + this, + &PhotoboothPreviewRender::renderingStopped, + Qt::UniqueConnection); }); } -PhotoboothPreviewRender::~PhotoboothPreviewRender() -{ - disconnect(rendererStoppedConnection_); -} - QString PhotoboothPreviewRender::takePhoto(int size) { diff --git a/src/previewrenderer.h b/src/previewrenderer.h index 3fb6ad564..d5d91c9c0 100644 --- a/src/previewrenderer.h +++ b/src/previewrenderer.h @@ -34,7 +34,7 @@ class PreviewRenderer : public QQuickPaintedItem public: explicit PreviewRenderer(QQuickItem* parent = nullptr); - ~PreviewRenderer(); + virtual ~PreviewRenderer(); Q_SIGNALS: void lrcInstanceChanged(); @@ -57,7 +57,7 @@ class VideoCallPreviewRenderer : public PreviewRenderer previewImageScalingFactorChanged) public: explicit VideoCallPreviewRenderer(QQuickItem* parent = nullptr); - virtual ~VideoCallPreviewRenderer(); + ~VideoCallPreviewRenderer(); Q_SIGNALS: void previewImageScalingFactorChanged(); @@ -73,15 +73,13 @@ class PhotoboothPreviewRender : public PreviewRenderer Q_OBJECT public: explicit PhotoboothPreviewRender(QQuickItem* parent = nullptr); - virtual ~PhotoboothPreviewRender(); + ~PhotoboothPreviewRender() = default; Q_INVOKABLE QString takePhoto(int size); Q_SIGNALS: - void hideBooth(); + void renderingStopped(); private: void paint(QPainter* painter) override final; - - QMetaObject::Connection rendererStoppedConnection_; }; diff --git a/src/quickimageproviderbase.h b/src/quickimageproviderbase.h index 8a612a140..896269d10 100644 --- a/src/quickimageproviderbase.h +++ b/src/quickimageproviderbase.h @@ -28,8 +28,8 @@ class QuickImageProviderBase : public QObject, public QQuickImageProvider { public: QuickImageProviderBase(QQuickImageProvider::ImageType type, - QQmlImageProviderBase::Flag flag, - LRCInstance* instance = nullptr) + QQmlImageProviderBase::Flag flag, + LRCInstance* instance = nullptr) : QQuickImageProvider(type, flag) , lrcInstance_(instance) {} diff --git a/src/searchresultslistmodel.cpp b/src/searchresultslistmodel.cpp index 8329524a9..48111fc63 100644 --- a/src/searchresultslistmodel.cpp +++ b/src/searchresultslistmodel.cpp @@ -52,6 +52,5 @@ void SearchResultsListModel::onSearchResultsUpdated() { beginResetModel(); - fillContactAvatarUidMap(lrcInstance_->getCurrentAccountInfo().contactModel->getAllContacts()); endResetModel(); } diff --git a/src/selectablelistproxymodel.cpp b/src/selectablelistproxymodel.cpp index fc2b34a69..eee4b03a3 100644 --- a/src/selectablelistproxymodel.cpp +++ b/src/selectablelistproxymodel.cpp @@ -97,14 +97,6 @@ SelectableListProxyModel::selectSourceRow(int row) updateSelection(); } -void -SelectableListProxyModel::updateContactAvatarUid(const QString& contactUri) -{ - auto base = qobject_cast<ConversationListModelBase*>(sourceModel()); - if (base) - base->updateContactAvatarUid(contactUri); -} - void SelectableListProxyModel::updateSelection(bool rowsRemoved) { diff --git a/src/selectablelistproxymodel.h b/src/selectablelistproxymodel.h index a8e775dd7..0d63ef703 100644 --- a/src/selectablelistproxymodel.h +++ b/src/selectablelistproxymodel.h @@ -42,10 +42,6 @@ public: Q_INVOKABLE QVariant dataForRow(int row, int role) const; void selectSourceRow(int row); - // this may not be the best place for this but it prevents a level of - // inheritance and prevents code duplication - Q_INVOKABLE void updateContactAvatarUid(const QString& contactUri); - public Q_SLOTS: void updateSelection(bool rowsRemoved = false); diff --git a/src/settingsview/components/AccountProfile.qml b/src/settingsview/components/AccountProfile.qml index 8ce48df6b..229904276 100644 --- a/src/settingsview/components/AccountProfile.qml +++ b/src/settingsview/components/AccountProfile.qml @@ -44,11 +44,6 @@ ColumnLayout { displayNameLineEdit.text = SettingsAdapter.getCurrentAccount_Profile_Info_Alias() } - function initPhotoBooth() { - currentAccountAvatar.initUI(false) - currentAccountAvatar.setAvatarImage() - } - function stopBooth() { currentAccountAvatar.stopBooth() } @@ -74,8 +69,9 @@ ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignCenter - saveToConfig: true - boothWidth: 180 + imageId: LRCInstance.currentAccountId + + size: 180 } MaterialLineEdit { diff --git a/src/settingsview/components/ContactItemDelegate.qml b/src/settingsview/components/ContactItemDelegate.qml index 36faaae89..b46097f54 100644 --- a/src/settingsview/components/ContactItemDelegate.qml +++ b/src/settingsview/components/ContactItemDelegate.qml @@ -43,8 +43,6 @@ ItemDelegate { color: highlighted? JamiTheme.selectedColor : JamiTheme.editBackgroundColor } - onContactIDChanged: avatarImg.updateImage(contactID) - RowLayout { anchors.fill: parent @@ -57,31 +55,14 @@ ItemDelegate { Layout.preferredWidth: JamiTheme.preferredFieldHeight Layout.preferredHeight: JamiTheme.preferredFieldHeight - background: Rectangle { + background: Avatar { + id: avatar + anchors.fill: parent - color: "transparent" - AvatarImage { - id: avatarImg - - anchors.fill: parent - - avatarMode: AvatarImage.AvatarMode.FromContactUri - showPresenceIndicator: false - - fillMode: Image.PreserveAspectCrop - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: avatarImg.width - height: avatarImg.height - radius: { - var size = ((avatarImg.width <= avatarImg.height) ? - avatarImg.width:avatarImg.height) - return size / 2 - } - } - } - } + + mode: Avatar.Mode.Contact + imageId: contactID + showPresenceIndicator: false } } diff --git a/src/settingsview/components/CurrentAccountSettings.qml b/src/settingsview/components/CurrentAccountSettings.qml index 8b85e7ac5..b8e6dc6eb 100644 --- a/src/settingsview/components/CurrentAccountSettings.qml +++ b/src/settingsview/components/CurrentAccountSettings.qml @@ -44,8 +44,6 @@ Rectangle { signal advancedSettingsToggled(bool settingsVisible) function updateAccountInfoDisplayed() { - accountProfile.initPhotoBooth() - accountEnableCheckBox.checked = SettingsAdapter.get_CurrentAccountInfo_Enabled() accountProfile.updateAccountInfo() userIdentity.updateAccountInfo() diff --git a/src/smartlistmodel.cpp b/src/smartlistmodel.cpp index 548a47f04..85b4f4535 100644 --- a/src/smartlistmodel.cpp +++ b/src/smartlistmodel.cpp @@ -157,8 +157,6 @@ void SmartListModel::fillConversationsList() { beginResetModel(); - fillContactAvatarUidMap(lrcInstance_->getCurrentAccountInfo().contactModel->getAllContacts()); - auto* convModel = lrcInstance_->getCurrentConversationModel(); using ConversationList = ConversationModel::ConversationQueueProxy; conversations_ = ConversationList(convModel->getAllSearchResults()) diff --git a/src/utils.cpp b/src/utils.cpp index aab3648ae..ddb03be5f 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -324,6 +324,28 @@ Utils::GetISODate() #endif } +QImage +Utils::accountPhoto(LRCInstance* instance, const QString& accountId, const QSize& size) +{ + QImage photo; + try { + auto& accInfo = instance->accountModel().getAccountInfo( + accountId.isEmpty() ? instance->get_currentAccountId() : accountId); + if (!accInfo.profileInfo.avatar.isEmpty()) { + photo = imageFromBase64String(accInfo.profileInfo.avatar); + } else { + auto bestName = instance->accountModel().bestNameForAccount(accInfo.id); + QString name = bestName == accInfo.profileInfo.uri ? QString() : bestName; + QString prefix = accInfo.profileInfo.type == profile::Type::JAMI ? "ring:" : "sip:"; + photo = fallbackAvatar(prefix + accInfo.profileInfo.uri, name, size); + } + } catch (const std::exception& e) { + qDebug() << e.what() << "; Using default avatar"; + photo = fallbackAvatar(QString(), QString(), size); + } + return Utils::scaleAndFrame(photo, size); +} + QImage Utils::contactPhoto(LRCInstance* instance, const QString& contactUri, @@ -331,26 +353,20 @@ Utils::contactPhoto(LRCInstance* instance, const QString& accountId) { QImage photo; - try { - /* - * Get first contact photo. - */ - auto& accountInfo = instance->accountModel().getAccountInfo( + auto& accInfo = instance->accountModel().getAccountInfo( accountId.isEmpty() ? instance->get_currentAccountId() : accountId); - auto contactInfo = accountInfo.contactModel->getContact(contactUri); + auto contactInfo = accInfo.contactModel->getContact(contactUri); auto contactPhoto = contactInfo.profileInfo.avatar; - auto bestName = accountInfo.contactModel->bestNameForContact(contactUri); - auto bestId = accountInfo.contactModel->bestIdForContact(contactUri); - if (accountInfo.profileInfo.type == lrc::api::profile::Type::SIP - && contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY) { + auto bestName = accInfo.contactModel->bestNameForContact(contactUri); + if (accInfo.profileInfo.type == profile::Type::SIP + && contactInfo.profileInfo.type == profile::Type::TEMPORARY) { photo = Utils::fallbackAvatar(QString(), QString()); - } else if (contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY + } else if (contactInfo.profileInfo.type == profile::Type::TEMPORARY && contactInfo.profileInfo.uri.isEmpty()) { photo = Utils::fallbackAvatar(QString(), QString()); } else if (!contactPhoto.isEmpty()) { - QByteArray byteArray = Utils::base64StringToByteArray(contactPhoto); - photo = contactPhotoFromBase64(byteArray, nullptr); + photo = imageFromBase64String(contactPhoto); if (photo.isNull()) { auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName; photo = Utils::fallbackAvatar("ring:" + contactInfo.profileInfo.uri, avatarName); @@ -360,21 +376,55 @@ Utils::contactPhoto(LRCInstance* instance, photo = Utils::fallbackAvatar("ring:" + contactInfo.profileInfo.uri, avatarName); } } catch (const std::exception& e) { - qDebug() << e.what(); + qDebug() << Q_FUNC_INFO << e.what(); } return Utils::scaleAndFrame(photo, size); } QImage -Utils::contactPhotoFromBase64(const QByteArray& data, const QString& type) +Utils::conversationAvatar(LRCInstance* instance, + const QString& convId, + const QSize& size, + const QString& accountId) { - QImage avatar; - const bool ret = avatar.loadFromData(data, type.toLatin1()); - if (!ret) { - qDebug() << "Utils: vCard image loading failed"; - return QImage(); + QImage avatar(size, QImage::Format_ARGB32_Premultiplied); + avatar.fill(Qt::transparent); + QPainter painter(&avatar); + painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); + try { + auto& accInfo = instance->accountModel().getAccountInfo( + accountId.isEmpty() ? instance->get_currentAccountId() : accountId); + auto* convModel = accInfo.conversationModel.get(); + Q_FOREACH (const auto peerUri, convModel->peersForConversation(convId)) { + auto peerAvatar = Utils::contactPhoto(instance, peerUri, size); + painter.drawImage(avatar.rect(), peerAvatar); + } + } catch (const std::exception& e) { + qDebug() << Q_FUNC_INFO << e.what(); + } + return Utils::scaleAndFrame(avatar, size); +} + +QImage +Utils::imageFromBase64String(const QString& str, bool circleCrop) +{ + return imageFromBase64Data(Utils::base64StringToByteArray(str), circleCrop); +} + +QImage +Utils::imageFromBase64Data(const QByteArray& data, bool circleCrop) +{ + QImage img; + + if (img.loadFromData(data)) { + if (circleCrop) { + return Utils::getCirclePhoto(img, img.size().width()); + } + return img; } - return Utils::getCirclePhoto(avatar, avatar.size().width()); + + qWarning() << Q_FUNC_INFO << "Image loading failed"; + return {}; } QImage @@ -558,48 +608,38 @@ Utils::getAvatarColor(const QString& canonicalUri) return JamiAvatarTheme::avatarColors_[colorIndex]; } -/* Generate a QImage representing a dummy user avatar, when user doesn't provide it. - * Current rendering is a flat colored circle with a centered letter. - * The color of the letter is computed from the circle color to be visible whaterver be the circle - * color. +/*! + * Generate a QImage representing a default user avatar, when the user doesn't provide it. + * If the name passed is empty, then the default avatar picture will be displayed instead + * of a letter. + * + * @param canonicalUri uri containing the account type prefix used to obtain the bgcolor + * @param name the string used to acquire the letter centered in the avatar + * @param size the dimensions of the desired image */ QImage -Utils::fallbackAvatar(const QString& canonicalUriStr, const QString& letterStr, const QSize& size) +Utils::fallbackAvatar(const QString& canonicalUri, const QString& name, const QSize& size) { auto sizeToUse = size.height() >= defaultAvatarSize.height() ? size : defaultAvatarSize; - /* - * We start with a transparent avatar. - */ QImage avatar(sizeToUse, QImage::Format_ARGB32); avatar.fill(Qt::transparent); - /* - * We pick a color based on the passed character. - */ - QColor avColor = getAvatarColor(canonicalUriStr); - - /* - * We draw a circle with this color. - */ QPainter painter(&avatar); painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); painter.setPen(Qt::transparent); - painter.setBrush(avColor.lighter(110)); + + // background circle + painter.setBrush(getAvatarColor(canonicalUri).lighter(110)); painter.drawEllipse(avatar.rect()); - /* - * If a letter was passed, then we paint a letter in the circle, - * otherwise we draw the default avatar icon. - */ - QString letterStrCleaned(letterStr); - letterStrCleaned.remove(QRegExp("[\\n\\t\\r]")); - if (!letterStr.isEmpty()) { - auto unicode = letterStr.toUcs4().at(0); + // if a letter was passed, then we paint a letter in the circle, + // otherwise we draw the default avatar icon + QString trimmedName(name); + if (!trimmedName.remove(QRegExp("[\\n\\t\\r]")).isEmpty()) { + auto unicode = trimmedName.toUcs4().at(0); if (unicode >= 0x1F000 && unicode <= 0x1FFFF) { - /* - * Is Emoticon. - */ + // emoticon auto letter = QString::fromUcs4(&unicode, 1); QFont font(QStringLiteral("Segoe UI Emoji"), avatar.height() / 2.66667, QFont::Medium); painter.setFont(font); @@ -607,10 +647,8 @@ Utils::fallbackAvatar(const QString& canonicalUriStr, const QString& letterStr, emojiRect.moveTop(-6); painter.drawText(emojiRect, letter, QTextOption(Qt::AlignCenter)); } else if (unicode >= 0x0000 && unicode <= 0x00FF) { - /* - * Is Basic Latin. - */ - auto letter = letterStr.at(0).toUpper(); + // basic Latin + auto letter = trimmedName.at(0).toUpper(); QFont font("Arial", avatar.height() / 2.66667, QFont::Medium); painter.setFont(font); painter.setPen(Qt::white); @@ -789,28 +827,6 @@ Utils::scaleAndFrame(const QImage photo, const QSize& size) return photo.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); } -QImage -Utils::accountPhoto(LRCInstance* instance, - const lrc::api::account::Info& accountInfo, - const QSize& size) -{ - QImage photo; - if (!accountInfo.profileInfo.avatar.isEmpty()) { - QByteArray ba = Utils::base64StringToByteArray(accountInfo.profileInfo.avatar); - photo = contactPhotoFromBase64(ba, nullptr); - } else { - auto bestId = instance->accountModel().bestIdForAccount(accountInfo.id); - auto bestName = instance->accountModel().bestNameForAccount(accountInfo.id); - QString letterStr = (bestId == bestName || bestName == accountInfo.profileInfo.uri) - ? QString() - : bestName; - QString prefix = accountInfo.profileInfo.type == lrc::api::profile::Type::JAMI ? "ring:" - : "sip:"; - photo = fallbackAvatar(prefix + accountInfo.profileInfo.uri, letterStr, size); - } - return scaleAndFrame(photo, size); -} - QString Utils::humanFileSize(qint64 fileSize) { @@ -844,5 +860,5 @@ Utils::isImage(const QString& fileExt) QString Utils::generateUid() { - return QUuid::createUuid().toString(); + return QUuid::createUuid().toString(QUuid::Id128); } diff --git a/src/utils.h b/src/utils.h index 99a9d54d0..3f0405331 100644 --- a/src/utils.h +++ b/src/utils.h @@ -80,16 +80,24 @@ bool isContactValid(const QString& contactUid, const lrc::api::ConversationModel bool getReplyMessageBox(QWidget* widget, const QString& title, const QString& text); // Image manipulation -static const QSize defaultAvatarSize {128, 128}; -QImage contactPhotoFromBase64(const QByteArray& data, const QString& type); +constexpr static const QSize defaultAvatarSize {128, 128}; +QImage imageFromBase64String(const QString& str, bool circleCrop = true); +QImage imageFromBase64Data(const QByteArray& data, bool circleCrop = true); +QImage accountPhoto(LRCInstance* instance, + const QString& accountId, + const QSize& size = defaultAvatarSize); QImage contactPhoto(LRCInstance* instance, const QString& contactUri, const QSize& size = defaultAvatarSize, const QString& accountId = {}); +QImage conversationAvatar(LRCInstance* instance, + const QString& convId, + const QSize& size = defaultAvatarSize, + const QString& accountId = {}); QImage getCirclePhoto(const QImage original, int sizePhoto); QColor getAvatarColor(const QString& canonicalUri); QImage fallbackAvatar(const QString& canonicalUriStr, - const QString& letterStr = QString(), + const QString& letterStr = {}, const QSize& size = defaultAvatarSize); QImage fallbackAvatar(const std::string& alias, const std::string& uri, @@ -101,9 +109,6 @@ QByteArray QByteArrayFromFile(const QString& filename); QPixmap generateTintedPixmap(const QString& filename, QColor color); QPixmap generateTintedPixmap(const QPixmap& pix, QColor color); QImage scaleAndFrame(const QImage photo, const QSize& size = defaultAvatarSize); -QImage accountPhoto(LRCInstance* instance, - const lrc::api::account::Info& accountInfo, - const QSize& size = defaultAvatarSize); QImage cropImage(const QImage& img); QPixmap pixmapFromSvg(const QString& svg_resource, const QSize& size); QImage setupQRCode(QString ringID, int margin); diff --git a/src/wizardview/WizardView.qml b/src/wizardview/WizardView.qml index 83da3b243..b8471bc28 100644 --- a/src/wizardview/WizardView.qml +++ b/src/wizardview/WizardView.qml @@ -392,7 +392,6 @@ Rectangle { } onSaveProfile: { - avatarBooth.manualSaveToConfig() AccountAdapter.setCurrAccDisplayName(profilePage.displayName) leave() } diff --git a/src/wizardview/components/ProfilePage.qml b/src/wizardview/components/ProfilePage.qml index d308c0756..e0dd93c10 100644 --- a/src/wizardview/components/ProfilePage.qml +++ b/src/wizardview/components/ProfilePage.qml @@ -27,7 +27,8 @@ import "../../commoncomponents" Rectangle { id: root - property string createdAccountId: "" + // trigger a default avatar prior to account generation + property string createdAccountId: "dummy" property int preferredHeight: profilePageColumnLayout.implicitHeight property var showBottom: false property alias displayName: aliasEdit.text @@ -38,7 +39,7 @@ Rectangle { signal saveProfile function initializeOnShowUp() { - setAvatarWidget.initUI() + createdAccountId = "dummy" clearAllTextFields() saveProfileBtn.spinnerTriggered = true } @@ -53,11 +54,6 @@ Rectangle { color: JamiTheme.backgroundColor - onCreatedAccountIdChanged: { - setAvatarWidget.setAvatarImage(AvatarImage.AvatarMode.FromAccount, - createdAccountId) - } - ColumnLayout { id: profilePageColumnLayout @@ -100,9 +96,11 @@ Rectangle { Layout.alignment: Qt.AlignCenter Layout.preferredWidth: size - Layout.preferredHeight: size + Layout.fillHeight: true + + imageId: createdAccountId - boothWidth: 200 + size: 200 } MaterialLineEdit { -- GitLab