Skip to content
Snippets Groups Projects
Commit 173cf2be authored by Ming Rui Zhang's avatar Ming Rui Zhang
Browse files

migration: use image provider to show avatar image

1. Use avatarimageprovider
2. Remove redundant base64 code

Change-Id: I2a2517890e95b4a9f9a363fbea2251d6d5dd1c8f
parent b4b56aec
No related branches found
No related tags found
No related merge requests found
Showing
with 375 additions and 102 deletions
......@@ -42,7 +42,6 @@ set(COMMON_SOURCES
src/main.cpp
src/smartlistmodel.cpp
src/utils.cpp
src/pixbufmanipulator.cpp
src/rendermanager.cpp
src/connectivitymonitor.cpp
src/mainapplication.cpp
......@@ -85,7 +84,6 @@ set(COMMON_HEADERS
src/globalsystemtray.h
src/appsettingsmanager.h
src/webchathelpers.h
src/pixbufmanipulator.h
src/rendermanager.h
src/connectivitymonitor.h
src/jamiavatartheme.h
......
......@@ -111,6 +111,7 @@ unix {
# Input
HEADERS += \
src/avatarimageprovider.h \
src/networkmanager.h \
src/smartlistmodel.h \
src/updatemanager.h \
......@@ -123,7 +124,6 @@ HEADERS += \
src/globalsystemtray.h \
src/appsettingsmanager.h \
src/webchathelpers.h \
src/pixbufmanipulator.h \
src/rendermanager.h \
src/connectivitymonitor.h \
src/jamiavatartheme.h \
......@@ -168,7 +168,6 @@ SOURCES += \
src/main.cpp \
src/smartlistmodel.cpp \
src/utils.cpp \
src/pixbufmanipulator.cpp \
src/rendermanager.cpp \
src/connectivitymonitor.cpp \
src/mainapplication.cpp \
......
......@@ -97,7 +97,6 @@
<file>src/mainview/components/ProjectCreditsScrollView.qml</file>
<file>src/mainview/components/AccountComboBoxPopup.qml</file>
<file>src/mainview/components/ConversationSmartListViewItemDelegate.qml</file>
<file>src/mainview/components/ConversationSmartListUserImage.qml</file>
<file>src/mainview/components/SidePanelTabBar.qml</file>
<file>src/mainview/components/WelcomePageQrDialog.qml</file>
<file>src/commoncomponents/GeneralMenuItem.qml</file>
......@@ -137,5 +136,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>
</qresource>
</RCC>
......@@ -373,13 +373,15 @@ AccountAdapter::connectAccount(const QString& accountId)
&lrc::api::NewAccountModel::profileUpdated,
[this](const QString& accountId) {
if (LRCInstance::getCurrAccId() == accountId)
emit accountStatusChanged();
emit accountStatusChanged(accountId);
});
accountStatusChangedConnection_
= QObject::connect(accInfo.accountModel,
&lrc::api::NewAccountModel::accountStatusChanged,
[this] { emit accountStatusChanged(); });
[this](const QString& accountId) {
emit accountStatusChanged(accountId);
});
contactAddedConnection_
= QObject::connect(accInfo.contactModel.get(),
......
......@@ -110,7 +110,7 @@ signals:
/*
* Trigger other components to reconnect account related signals.
*/
void accountStatusChanged();
void accountStatusChanged(QString accountId = {});
void updateConversationForAddedContact();
/*
* send report failure to QML to make it show the right UI state .
......
......@@ -21,10 +21,7 @@
#include <QDateTime>
#include "globalinstances.h"
#include "lrcinstance.h"
#include "pixbufmanipulator.h"
#include "utils.h"
AccountListModel::AccountListModel(QObject* parent)
......@@ -68,6 +65,8 @@ AccountListModel::data(const QModelIndex& index, int role) const
auto& accountInfo = LRCInstance::accountModel().getAccountInfo(accountList.at(index.row()));
// 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(Utils::bestNameForAccount(accountInfo));
......@@ -77,11 +76,10 @@ AccountListModel::data(const QModelIndex& index, int role) const
return QVariant(static_cast<int>(accountInfo.profileInfo.type));
case Role::Status:
return QVariant(static_cast<int>(accountInfo.status));
case Role::Picture:
return QString::fromLatin1(
Utils::QImageToByteArray(Utils::accountPhoto(accountInfo)).toBase64().data());
case Role::ID:
return QVariant(accountInfo.id);
case Role::PictureUid:
return avatarUidMap_[accountInfo.id];
}
return QVariant();
}
......@@ -92,10 +90,10 @@ AccountListModel::roleNames() const
QHash<int, QByteArray> roles;
roles[Alias] = "Alias";
roles[Username] = "Username";
roles[Picture] = "Picture";
roles[Type] = "Type";
roles[Status] = "Status";
roles[ID] = "ID";
roles[PictureUid] = "PictureUid";
return roles;
}
......@@ -134,5 +132,28 @@ 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());
}
}
}
......@@ -30,7 +30,7 @@ class AccountListModel : public QAbstractListModel
Q_OBJECT
public:
enum Role { Alias = Qt::UserRole + 1, Username, Picture, Type, Status, ID };
enum Role { Alias = Qt::UserRole + 1, Username, Type, Status, ID, PictureUid };
Q_ENUM(Role)
explicit AccountListModel(QObject* parent = 0);
......@@ -55,4 +55,17 @@ public:
* This function is to 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);
private:
/*
* Give a uuid for each account avatar and it will serve PictureUid role
*/
void fillAvatarUidMap(const QStringList& accountList);
QMap<QString, QString> avatarUidMap_;
};
......@@ -92,9 +92,6 @@ AccountsToMigrateListModel::data(const QModelIndex& index, int role) const
return QVariant(avatarInfo.confProperties.username);
case Role::Alias:
return QVariant(LRCInstance::accountModel().getAccountInfo(accountId).profileInfo.alias);
case Role::Picture:
return QString::fromLatin1(
Utils::QImageToByteArray(Utils::accountPhoto(avatarInfo)).toBase64().data());
}
return QVariant();
}
......@@ -108,7 +105,6 @@ AccountsToMigrateListModel::roleNames() const
roles[ManagerUri] = "ManagerUri";
roles[Username] = "Username";
roles[Alias] = "Alias";
roles[Picture] = "Picture";
return roles;
}
......
......@@ -31,14 +31,7 @@ class AccountsToMigrateListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Role {
Account_ID = Qt::UserRole + 1,
ManagerUsername,
ManagerUri,
Username,
Alias,
Picture
};
enum Role { Account_ID = Qt::UserRole + 1, ManagerUsername, ManagerUri, Username, Alias };
Q_ENUM(Role)
explicit AccountsToMigrateListModel(QObject* parent = 0);
......
......@@ -16,53 +16,56 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import net.jami.Models 1.0
import "../../commoncomponents"
#pragma once
Image {
id: userImage
#include "utils.h"
width: 40
height: 40
#include <QImage>
#include <QQuickImageProvider>
fillMode: Image.PreserveAspectFit
source: "data:image/png;base64," + Picture
mipmap: true
class AvatarImageProvider : public QObject, public QQuickImageProvider
{
public:
AvatarImageProvider()
: QQuickImageProvider(QQuickImageProvider::Image,
QQmlImageProviderBase::ForceAsynchronousImageLoading)
{}
PresenceIndicator {
anchors.right: userImage.right
anchors.bottom: userImage.bottom
visible: Presence === undefined ? false : Presence
}
Rectangle {
id: unreadMessageCountRect
anchors.right: userImage.right
anchors.rightMargin: -2
anchors.top: userImage.top
anchors.topMargin: -2
width: 14
height: 14
visible: UnreadMessagesCount > 0
Text {
id: unreadMessageCounttext
/*
* Request function
* id could be
* 1. account_ + account id
* 2. file_ + file path
* 3. contact_+ contact uri
* 4. conversation_+ conversation uid
*/
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override
{
Q_UNUSED(size)
anchors.centerIn: unreadMessageCountRect
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);
text: UnreadMessagesCount > 9 ? "···" : UnreadMessagesCount
color: "white"
font.pointSize: JamiTheme.textFontSize
}
if (idContent.isEmpty())
return QImage();
radius: 30
color: JamiTheme.notificationRed
if (idType == "account") {
return Utils::accountPhoto(LRCInstance::accountModel().getAccountInfo(idContent),
requestedSize);
} else if (idType == "conversation") {
auto* convModel = LRCInstance::getCurrentAccountInfo().conversationModel.get();
const auto& conv = convModel->getConversationForUID(idContent);
return Utils::contactPhoto(conv.participants[0], requestedSize);
} else if (idType == "contact") {
return Utils::contactPhoto(idContent, requestedSize);
} else {
auto image = Utils::cropImage(QImage(idContent));
return image.scaled(requestedSize,
Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
}
}
};
......@@ -61,12 +61,6 @@ BannedListModel::data(const QModelIndex& index, int role) const
return QVariant(contactInfo.registeredName);
case Role::ContactID:
return QVariant(contactInfo.profileInfo.uri);
case Role::ContactPicture:
QImage avatarImage = Utils::fallbackAvatar(contactInfo.profileInfo.uri,
contactInfo.registeredName,
QSize(48, 48));
return QString::fromLatin1(Utils::QImageToByteArray(avatarImage).toBase64().data());
}
return QVariant();
}
......@@ -77,7 +71,6 @@ BannedListModel::roleNames() const
QHash<int, QByteArray> roles;
roles[ContactName] = "ContactName";
roles[ContactID] = "ContactID";
roles[ContactPicture] = "ContactPicture";
return roles;
}
......
......@@ -27,7 +27,7 @@ class BannedListModel : public QAbstractListModel
BannedListModel(const BannedListModel& cpy);
public:
enum Role { ContactName = Qt::UserRole + 1, ContactID, ContactPicture };
enum Role { ContactName = Qt::UserRole + 1, ContactID };
Q_ENUM(Role)
explicit BannedListModel(QObject* parent = nullptr);
......
......@@ -42,7 +42,6 @@ Window {
property bool nonOperationClosing: true
property bool successState : true
property string imgBase64: ""
signal accountMigrationFinished
......@@ -88,8 +87,7 @@ Window {
accountID = accountsToMigrateListModel.data(accountsToMigrateListModel.index(
0, 0), AccountsToMigrateListModel.Account_ID)
imgBase64 = accountsToMigrateListModel.data(accountsToMigrateListModel.index(
0, 0), AccountsToMigrateListModel.Picture)
avatarImg.updateImage(accountID)
connectionMigrationEnded.enabled = false
migrationPushButton.enabled = false
......@@ -284,17 +282,13 @@ Window {
anchors.fill: parent
color: "transparent"
Image {
AvatarImage {
id: avatarImg
anchors.fill: parent
source: {
if (imgBase64.length === 0) {
return ""
} else {
return "data:image/png;base64," + imgBase64
}
}
showPresenceIndicator: false
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: OpacityMask {
......
/*
* 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 QtQuick.Window 2.14
import net.jami.Models 1.0
Item {
id: root
// FromUrl here is for grabToImage image url
enum Mode {
FromAccount = 0,
FromFile,
FromContactUri,
FromConvUid,
FromUrl,
Default
}
property alias fillMode: rootImage.fillMode
property alias sourceSize: rootImage.sourceSize
property int mode: AvatarImage.Mode.FromAccount
property string imageProviderIdPrefix: {
switch(mode) {
case AvatarImage.Mode.FromAccount:
return "account_"
case AvatarImage.Mode.FromFile:
return "file_"
case AvatarImage.Mode.FromContactUri:
return "contact_"
case AvatarImage.Mode.FromConvUid:
return "conversation_"
default:
return ""
}
}
// Full request url example: forceUpdateUrl_xxxxxxx_account_xxxxxxxx
property string imageProviderUrl: "image://avatarImage/" + forceUpdateUrl + "_" +
imageProviderIdPrefix
property string imageId: ""
property string defaultImgUrl: "qrc:/images/default_avatar_overlay.svg"
property string forceUpdateUrl: Date.now()
property alias presenceStatus: presenceIndicator.status
property bool showPresenceIndicator: true
property int unreadMessagesCount: 0
signal imageIsReady
function updateImage(updatedId, oneTimeForceUpdateUrl) {
imageId = updatedId
if (oneTimeForceUpdateUrl === undefined)
forceUpdateUrl = Date.now()
else
forceUpdateUrl = oneTimeForceUpdateUrl
if (mode === AvatarImage.Mode.FromUrl)
rootImage.source = imageId
else if (imageId)
rootImage.source = imageProviderUrl + imageId
}
onModeChanged: {
if (mode === AvatarImage.Mode.Default)
rootImage.source = defaultImgUrl
}
Image {
id: rootImage
anchors.fill: root
smooth: false
antialiasing: true
sourceSize.width: Math.max(24, width)
sourceSize.height: Math.max(24, height)
fillMode: Image.PreserveAspectFit
onStatusChanged: {
if (status === Image.Ready) {
rootImageOverlay.state = ""
rootImageOverlay.state = "rootImageLoading"
}
}
Component.onCompleted: {
if (imageId)
return source = imageProviderUrl + imageId
return source = ""
}
Image {
id: rootImageOverlay
anchors.fill: rootImage
smooth: false
antialiasing: true
sourceSize.width: Math.max(24, width)
sourceSize.height: Math.max(24, height)
fillMode: Image.PreserveAspectFit
onOpacityChanged: {
if (opacity === 0)
source = rootImage.source
}
onStatusChanged: {
if (status === Image.Ready && opacity === 0) {
opacity = 1
root.imageIsReady()
}
}
states: State {
name: "rootImageLoading"
PropertyChanges { target: rootImageOverlay; opacity: 0}
}
transitions: Transition {
NumberAnimation { properties: "opacity"; easing.type: Easing.InOutQuad; duration: 400}
}
}
}
PresenceIndicator {
id: presenceIndicator
anchors.right: root.right
anchors.bottom: root.bottom
size: root.width * 0.3
visible: showPresenceIndicator
}
Rectangle {
id: unreadMessageCountRect
anchors.right: root.right
anchors.top: root.top
width: root.width * 0.3
height: root.width * 0.3
visible: unreadMessagesCount > 0
Text {
id: unreadMessageCounttext
anchors.centerIn: unreadMessageCountRect
text: unreadMessagesCount > 9 ? "" : unreadMessagesCount
color: "white"
font.pointSize: JamiTheme.textFontSize - 2
}
radius: 30
color: JamiTheme.notificationRed
}
}
......@@ -10,9 +10,10 @@ import net.jami.Adapters 1.0
ColumnLayout {
property bool takePhotoState: false
property bool hasAvatar: false
property bool isDefaultIcon: false
property string imgBase64: ""
// saveToConfig is to specify whether the image should be saved to account config
property bool saveToConfig: false
property string fileName: ""
property var boothImg: ""
property int boothWidth: 224
......@@ -20,9 +21,6 @@ ColumnLayout {
buttonsRowLayout.height +
JamiTheme.preferredMarginSize / 2
signal imageAcquired
signal imageCleared
function startBooth(force = false){
hasAvatar = false
AccountAdapter.startPreviewing(force)
......@@ -39,12 +37,15 @@ ColumnLayout {
takePhotoState = false
}
function setAvatarPixmap(avatarPixmapBase64, defaultValue = false){
imgBase64 = avatarPixmapBase64
stopBooth()
if(defaultValue){
isDefaultIcon = defaultValue
}
function setAvatarImage(mode = AvatarImage.Mode.FromAccount,
imageId = AccountAdapter.currentAccountId){
if (mode === AvatarImage.Mode.Default)
boothImg = ""
avatarImg.mode = mode
if (imageId)
avatarImg.updateImage(imageId)
}
onVisibleChanged: {
......@@ -68,14 +69,13 @@ ColumnLayout {
onAccepted: {
fileName = file
if (fileName.length === 0) {
imageCleared()
SettingsAdapter.clearCurrentAvatar()
setAvatarImage()
return
}
imgBase64 = UtilsAdapter.getCroppedImageBase64FromFile(
UtilsAdapter.getAbsPath(fileName),
boothWidth)
imageAcquired()
stopBooth()
setAvatarImage(AvatarImage.Mode.FromFile,
UtilsAdapter.getAbsPath(fileName))
}
}
......@@ -96,29 +96,40 @@ ColumnLayout {
color: "grey"
radius: height / 2
Image {
AvatarImage {
id: avatarImg
anchors.fill: parent
source: {
if(imgBase64.length === 0){
return "qrc:/images/default_avatar_overlay.svg"
} else {
return "data:image/png;base64," + imgBase64
}
}
imageId: AccountAdapter.currentAccountId
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)
var size = ((avatarImg.width <= avatarImg.height) ?
avatarImg.width:avatarImg.height)
return size / 2
}
}
}
onImageIsReady: {
// Once image is loaded (updated), save to boothImg
avatarImg.grabToImage(function(result) {
if (mode !== AvatarImage.Mode.Default)
boothImg = result.image
if (saveToConfig)
SettingsAdapter.setCurrAccAvatar(result.image)
})
}
}
}
}
......@@ -126,9 +137,7 @@ ColumnLayout {
PhotoboothPreviewRender {
id:previewWidget
onHideBooth:{
stopBooth()
}
onHideBooth: stopBooth()
visible: takePhotoState
focus: visible
......@@ -143,7 +152,8 @@ ColumnLayout {
width: previewWidget.width
height: previewWidget.height
radius: {
var size = ((previewWidget.width <= previewWidget.height)? previewWidget.width:previewWidget.height)
var size = ((previewWidget.width <= previewWidget.height) ?
previewWidget.width:previewWidget.height)
return size / 2
}
}
......@@ -191,7 +201,6 @@ ColumnLayout {
radius: height / 6
source: {
if(takePhotoState) {
toolTipText = qsTr("Take photo")
return cameraAltIconUrl
......@@ -205,9 +214,9 @@ ColumnLayout {
return addPhotoIconUrl
}
}
onClicked: {
if(!takePhotoState){
imageCleared()
startBooth()
return
} else {
......@@ -215,11 +224,13 @@ ColumnLayout {
flashOverlay.visible = true
flashAnimation.restart()
// run concurrent function call to take photo
imgBase64 = previewWidget.takeCroppedPhotoToBase64(boothWidth)
previewWidget.grabToImage(function(result) {
setAvatarImage(AvatarImage.Mode.FromUrl, result.url)
hasAvatar = true
imageAcquired()
stopBooth()
})
}
}
}
......
......@@ -209,6 +209,14 @@ ConversationsAdapter::connectConversationModel(bool updateFilter)
emit modelSorted(QVariant::fromValue(conversation.uid));
});
contactProfileUpdatedConnection_
= QObject::connect(LRCInstance::getCurrentAccountInfo().contactModel.get(),
&lrc::api::ContactModel::profileUpdated,
[this](const QString& contactUri) {
conversationSmartListModel_->updateContactAvatarUid(contactUri);
emit updateListViewRequested();
});
modelUpdatedConnection_ = QObject::connect(currentConversationModel,
&lrc::api::ConversationModel::conversationUpdated,
[this](const QString& convUid) {
......@@ -295,6 +303,7 @@ ConversationsAdapter::disconnectConversationModel()
QObject::disconnect(interactionRemovedConnection_);
QObject::disconnect(searchStatusChangedConnection_);
QObject::disconnect(searchResultUpdatedConnection_);
QObject::disconnect(contactProfileUpdatedConnection_);
}
void
......
......@@ -82,6 +82,7 @@ private:
QMetaObject::Connection newConversationConnection_;
QMetaObject::Connection conversationRemovedConnection_;
QMetaObject::Connection conversationClearedConnection;
QMetaObject::Connection contactProfileUpdatedConnection_;
QMetaObject::Connection selectedCallChanged_;
QMetaObject::Connection smartlistSelectionConnection_;
QMetaObject::Connection interactionRemovedConnection_;
......
......@@ -336,15 +336,6 @@ public:
return -1;
}
static const QPixmap getCurrAccPixmap()
{
return instance()
.accountListModel_
.data(instance().accountListModel_.index(getCurrentAccountIndex()),
AccountListModel::Role::Picture)
.value<QPixmap>();
}
static void setAvatarForAccount(const QPixmap& avatarPixmap, const QString& accountID)
{
QByteArray ba;
......
......@@ -26,10 +26,8 @@
#include "globalsystemtray.h"
#include "qmlregister.h"
#include "qrimageprovider.h"
#include "pixbufmanipulator.h"
#include "tintedbuttonimageprovider.h"
#include "globalinstances.h"
#include "avatarimageprovider.h"
#include <QAction>
#include <QCommandLineParser>
......@@ -148,7 +146,6 @@ MainApplication::init()
gnutls_global_init();
#endif
GlobalInstances::setPixmapManipulator(std::make_unique<PixbufManipulator>());
initLrc(results[opts::UPDATEURL].toString(), connectivityMonitor_);
#ifdef Q_OS_WIN
......@@ -322,6 +319,7 @@ MainApplication::initQmlEngine()
engine_->addImageProvider(QLatin1String("qrImage"), new QrImageProvider());
engine_->addImageProvider(QLatin1String("tintedPixmap"), new TintedButtonImageProvider());
engine_->addImageProvider(QLatin1String("avatarImage"), new AvatarImageProvider());
engine_->load(QUrl(QStringLiteral("qrc:/src/MainApplicationWindow.qml")));
}
......
......@@ -308,8 +308,8 @@ Window {
mainViewWindowSidePanel.forceReselectConversationSmartListCurrentIndex()
}
function onAccountStatusChanged() {
accountComboBox.resetAccountListModel()
function onAccountStatusChanged(accountId) {
accountComboBox.resetAccountListModel(accountId)
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment