From 173cf2be50b794e41ead9aa5bf49fde00c755dec Mon Sep 17 00:00:00 2001
From: Ming Rui Zhang <mingrui.zhang@savoirfairelinux.com>
Date: Mon, 19 Oct 2020 14:51:31 -0400
Subject: [PATCH] migration: use image provider to show avatar image

1. Use avatarimageprovider
2. Remove redundant base64 code

Change-Id: I2a2517890e95b4a9f9a363fbea2251d6d5dd1c8f
---
 CMakeLists.txt                                |   2 -
 jami-qt.pro                                   |   3 +-
 qml.qrc                                       |   2 +-
 src/accountadapter.cpp                        |   6 +-
 src/accountadapter.h                          |   2 +-
 src/accountlistmodel.cpp                      |  35 +++-
 src/accountlistmodel.h                        |  15 +-
 src/accountstomigratelistmodel.cpp            |   4 -
 src/accountstomigratelistmodel.h              |   9 +-
 src/avatarimageprovider.h                     |  71 +++++++
 src/bannedlistmodel.cpp                       |   7 -
 src/bannedlistmodel.h                         |   2 +-
 .../AccountMigrationDialog.qml                |  16 +-
 src/commoncomponents/AvatarImage.qml          | 183 ++++++++++++++++++
 src/commoncomponents/PhotoboothView.qml       |  89 +++++----
 src/conversationsadapter.cpp                  |  11 +-
 src/conversationsadapter.h                    |   1 +
 src/lrcinstance.h                             |   9 -
 src/mainapplication.cpp                       |   6 +-
 src/mainview/MainView.qml                     |   4 +-
 src/mainview/components/AccountComboBox.qml   |  39 ++--
 .../components/AccountComboBoxPopup.qml       |  33 +---
 src/mainview/components/AudioCallPage.qml     |  11 +-
 src/mainview/components/ContactPicker.qml     |   2 -
 .../components/ContactPickerItemDelegate.qml  |   7 +-
 .../ConversationSmartListUserImage.qml        |  68 -------
 .../components/ConversationSmartListView.qml  |   2 +
 .../ConversationSmartListViewItemDelegate.qml |  23 ++-
 .../components/ParticipantOverlay.qml         |   1 +
 src/mainview/components/UserInfoCallPage.qml  |  11 +-
 src/mainview/components/UserProfile.qml       |  15 +-
 src/messagesadapter.cpp                       |   4 +-
 src/pixbufmanipulator.cpp                     | 127 ------------
 src/pixbufmanipulator.h                       |  49 -----
 src/previewrenderer.cpp                       |  18 --
 src/previewrenderer.h                         |   3 -
 src/settingsadapter.cpp                       |  25 +--
 src/settingsadapter.h                         |   3 +-
 .../components/AccountProfile.qml             |  10 +-
 .../components/BannedContacts.qml             |   1 -
 .../components/BannedItemDelegate.qml         |  10 +-
 src/smartlistmodel.cpp                        |  72 +++++--
 src/smartlistmodel.h                          |  16 +-
 src/utils.cpp                                 |  84 +++++---
 src/utils.h                                   |   7 +-
 src/utilsadapter.cpp                          |  17 --
 src/utilsadapter.h                            |   2 -
 src/wizardview/WizardView.qml                 |   7 +-
 .../components/CreateSIPAccountPage.qml       |   2 -
 src/wizardview/components/ProfilePage.qml     |   5 +-
 50 files changed, 588 insertions(+), 563 deletions(-)
 create mode 100644 src/avatarimageprovider.h
 create mode 100644 src/commoncomponents/AvatarImage.qml
 delete mode 100644 src/mainview/components/ConversationSmartListUserImage.qml
 delete mode 100644 src/pixbufmanipulator.cpp
 delete mode 100644 src/pixbufmanipulator.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9f9287b6c..ca7666e19 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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
diff --git a/jami-qt.pro b/jami-qt.pro
index 20dfb4945..473442d67 100644
--- a/jami-qt.pro
+++ b/jami-qt.pro
@@ -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 \
diff --git a/qml.qrc b/qml.qrc
index e9cc0f527..1c03e350e 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -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>
diff --git a/src/accountadapter.cpp b/src/accountadapter.cpp
index 269ee2ef3..f88a4ade0 100644
--- a/src/accountadapter.cpp
+++ b/src/accountadapter.cpp
@@ -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(),
diff --git a/src/accountadapter.h b/src/accountadapter.h
index 291f067a4..3d45333b3 100644
--- a/src/accountadapter.h
+++ b/src/accountadapter.h
@@ -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 .
diff --git a/src/accountlistmodel.cpp b/src/accountlistmodel.cpp
index 389037a66..c397b0f9e 100644
--- a/src/accountlistmodel.cpp
+++ b/src/accountlistmodel.cpp
@@ -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());
+        }
+    }
+}
diff --git a/src/accountlistmodel.h b/src/accountlistmodel.h
index 6741871d1..b1d247892 100644
--- a/src/accountlistmodel.h
+++ b/src/accountlistmodel.h
@@ -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_;
 };
diff --git a/src/accountstomigratelistmodel.cpp b/src/accountstomigratelistmodel.cpp
index 98fd1a99c..be9309fc2 100644
--- a/src/accountstomigratelistmodel.cpp
+++ b/src/accountstomigratelistmodel.cpp
@@ -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;
 }
 
diff --git a/src/accountstomigratelistmodel.h b/src/accountstomigratelistmodel.h
index 71abed754..6a35f80d1 100644
--- a/src/accountstomigratelistmodel.h
+++ b/src/accountstomigratelistmodel.h
@@ -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);
diff --git a/src/avatarimageprovider.h b/src/avatarimageprovider.h
new file mode 100644
index 000000000..b0cafc1c2
--- /dev/null
+++ b/src/avatarimageprovider.h
@@ -0,0 +1,71 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include "utils.h"
+
+#include <QImage>
+#include <QQuickImageProvider>
+
+class AvatarImageProvider : public QObject, public QQuickImageProvider
+{
+public:
+    AvatarImageProvider()
+        : QQuickImageProvider(QQuickImageProvider::Image,
+                              QQmlImageProviderBase::ForceAsynchronousImageLoading)
+    {}
+
+    /*
+     * 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)
+
+        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())
+            return QImage();
+
+        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);
+        }
+    }
+};
diff --git a/src/bannedlistmodel.cpp b/src/bannedlistmodel.cpp
index b07e6cb64..684f5b443 100644
--- a/src/bannedlistmodel.cpp
+++ b/src/bannedlistmodel.cpp
@@ -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;
 }
 
diff --git a/src/bannedlistmodel.h b/src/bannedlistmodel.h
index f664b9b55..28a2951e4 100644
--- a/src/bannedlistmodel.h
+++ b/src/bannedlistmodel.h
@@ -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);
diff --git a/src/commoncomponents/AccountMigrationDialog.qml b/src/commoncomponents/AccountMigrationDialog.qml
index 838f11319..e9e8aae43 100644
--- a/src/commoncomponents/AccountMigrationDialog.qml
+++ b/src/commoncomponents/AccountMigrationDialog.qml
@@ -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 {
diff --git a/src/commoncomponents/AvatarImage.qml b/src/commoncomponents/AvatarImage.qml
new file mode 100644
index 000000000..28497a438
--- /dev/null
+++ b/src/commoncomponents/AvatarImage.qml
@@ -0,0 +1,183 @@
+/*
+ * 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
+    }
+
+}
diff --git a/src/commoncomponents/PhotoboothView.qml b/src/commoncomponents/PhotoboothView.qml
index f28004938..1b7e2a0ef 100644
--- a/src/commoncomponents/PhotoboothView.qml
+++ b/src/commoncomponents/PhotoboothView.qml
@@ -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)
-                            return size /2
+                            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,8 +152,9 @@ ColumnLayout {
                 width: previewWidget.width
                 height: previewWidget.height
                 radius: {
-                    var size = ((previewWidget.width <= previewWidget.height)? previewWidget.width:previewWidget.height)
-                    return size /2
+                    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)
-                    hasAvatar = true
-                    imageAcquired()
-                    stopBooth()
+                    previewWidget.grabToImage(function(result) {
+
+                        setAvatarImage(AvatarImage.Mode.FromUrl, result.url)
+
+                        hasAvatar = true
+                        stopBooth()
+                    })
                 }
             }
         }
diff --git a/src/conversationsadapter.cpp b/src/conversationsadapter.cpp
index b9333b272..27d836400 100644
--- a/src/conversationsadapter.cpp
+++ b/src/conversationsadapter.cpp
@@ -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) {
@@ -262,7 +270,7 @@ ConversationsAdapter::connectConversationModel(bool updateFilter)
                            &lrc::api::ConversationModel::searchStatusChanged,
                            [this](const QString& status) { emit showSearchStatus(status); });
 
-    // This connection is ideal when  separated search results list.
+    // This connection is ideal when separated search results list.
     // This signal is guaranteed to fire just after filterChanged during a search if results are
     // changed, and once before filterChanged when calling setFilter.
     // NOTE: Currently, when searching, the entire conversation list will be copied 2-3 times each
@@ -295,6 +303,7 @@ ConversationsAdapter::disconnectConversationModel()
     QObject::disconnect(interactionRemovedConnection_);
     QObject::disconnect(searchStatusChangedConnection_);
     QObject::disconnect(searchResultUpdatedConnection_);
+    QObject::disconnect(contactProfileUpdatedConnection_);
 }
 
 void
diff --git a/src/conversationsadapter.h b/src/conversationsadapter.h
index abd97368a..6fc23a0a0 100644
--- a/src/conversationsadapter.h
+++ b/src/conversationsadapter.h
@@ -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_;
diff --git a/src/lrcinstance.h b/src/lrcinstance.h
index ebebd20da..909806e83 100644
--- a/src/lrcinstance.h
+++ b/src/lrcinstance.h
@@ -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;
diff --git a/src/mainapplication.cpp b/src/mainapplication.cpp
index 654387d2d..33b3f2d4c 100644
--- a/src/mainapplication.cpp
+++ b/src/mainapplication.cpp
@@ -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")));
 }
diff --git a/src/mainview/MainView.qml b/src/mainview/MainView.qml
index a757276c2..3d61b27b1 100644
--- a/src/mainview/MainView.qml
+++ b/src/mainview/MainView.qml
@@ -308,8 +308,8 @@ Window {
                             mainViewWindowSidePanel.forceReselectConversationSmartListCurrentIndex()
                         }
 
-                        function onAccountStatusChanged() {
-                            accountComboBox.resetAccountListModel()
+                        function onAccountStatusChanged(accountId) {
+                            accountComboBox.resetAccountListModel(accountId)
                         }
                     }
 
diff --git a/src/mainview/components/AccountComboBox.qml b/src/mainview/components/AccountComboBox.qml
index 53f4dff94..76f2beb88 100644
--- a/src/mainview/components/AccountComboBox.qml
+++ b/src/mainview/components/AccountComboBox.qml
@@ -31,7 +31,8 @@ ComboBox {
     signal settingBtnClicked
 
     // Reset accountListModel.
-    function resetAccountListModel() {
+    function resetAccountListModel(accountId) {
+        accountListModel.updateAvatarUid(accountId)
         accountListModel.reset()
     }
 
@@ -39,9 +40,11 @@ ComboBox {
         target: accountListModel
 
         function onModelReset() {
-            userImageRoot.source = "data:image/png;base64," + accountListModel.data(
-                        accountListModel.index(0, 0), AccountListModel.Picture)
-            currentAccountPresenceIndicator.status =
+            userImageRoot.updateImage(
+                        AccountAdapter.currentAccountId,
+                        accountListModel.data(
+                            accountListModel.index(0, 0), AccountListModel.PictureUid))
+            userImageRoot.presenceStatus =
                     accountListModel.data(accountListModel.index(0, 0), AccountListModel.Status)
             textMetricsUserAliasRoot.text = accountListModel.data(accountListModel.index(0,0),
                                                                   AccountListModel.Alias)
@@ -50,34 +53,20 @@ ComboBox {
         }
     }
 
-    Image {
+    AvatarImage {
         id: userImageRoot
 
         anchors.left: root.left
         anchors.leftMargin: 16
         anchors.verticalCenter: root.verticalCenter
 
-        width: 30
-        height: 30
+        width: 40
+        height: 40
 
-        fillMode: Image.PreserveAspectFit
+        imageId: AccountAdapter.currentAccountId
 
-        // Base 64 format
-        source: "data:image/png;base64," + accountListModel.data(
-                            accountListModel.index(0, 0), AccountListModel.Picture)
-        mipmap: true
-
-        PresenceIndicator {
-            id: currentAccountPresenceIndicator
-
-            anchors.right: userImageRoot.right
-            anchors.rightMargin: -2
-            anchors.bottom: userImageRoot.bottom
-            anchors.bottomMargin: -2
-
-            status: accountListModel.data(accountListModel.index(0, 0),
-                                                 AccountListModel.Status)
-        }
+        presenceStatus: accountListModel.data(accountListModel.index(0, 0),
+                                              AccountListModel.Status)
     }
 
     Text {
@@ -251,8 +240,6 @@ ComboBox {
         }
     }
 
-
-
     indicator: null
 
     // Overwrite the combo box pop up to add footer (for add accounts).
diff --git a/src/mainview/components/AccountComboBoxPopup.qml b/src/mainview/components/AccountComboBoxPopup.qml
index a40318cf0..70869f91e 100644
--- a/src/mainview/components/AccountComboBoxPopup.qml
+++ b/src/mainview/components/AccountComboBoxPopup.qml
@@ -45,42 +45,29 @@ Popup {
     contentItem: ListView {
         id: comboBoxPopupListView
 
-
         // In list view, index is an interger.
         clip: true
         model: accountListModel
         implicitHeight: contentHeight
         delegate: ItemDelegate {
-            Image {
+            AvatarImage {
                 id: userImage
 
                 anchors.left: parent.left
                 anchors.leftMargin: 10
                 anchors.verticalCenter: parent.verticalCenter
 
-                width: 30
-                height: 30
-
-                fillMode: Image.PreserveAspectFit
-                mipmap: true
-
-                // Role::Picture
-                source: {
-                    var data = accountListModel.data(accountListModel.index(index, 0),
-                                                     AccountListModel.Picture)
-                    if (data === undefined) {
-                        return ""
-                    }
-                    return "data:image/png;base64," + data
-                }
+                width: 40
+                height: 40
 
-                PresenceIndicator {
-                    anchors.right: userImage.right
-                    anchors.rightMargin: -2
-                    anchors.bottom: userImage.bottom
-                    anchors.bottomMargin: -2
+                presenceStatus: Status
 
-                    status: Status
+                Component.onCompleted: {
+                    return updateImage(
+                                accountListModel.data(
+                                    accountListModel.index(index, 0), AccountListModel.ID),
+                                accountListModel.data(
+                                    accountListModel.index(index, 0), AccountListModel.PictureUid))
                 }
             }
 
diff --git a/src/mainview/components/AudioCallPage.qml b/src/mainview/components/AudioCallPage.qml
index 56a7ed126..74508ac77 100644
--- a/src/mainview/components/AudioCallPage.qml
+++ b/src/mainview/components/AudioCallPage.qml
@@ -28,7 +28,6 @@ import "../../commoncomponents"
 Rectangle {
     id: audioCallPageRect
 
-    property string contactImgSource: ""
     property string bestName: "Best Name"
     property string bestId: "Best Id"
 
@@ -37,8 +36,7 @@ Rectangle {
     signal showFullScreenReqested
 
     function updateUI(accountId, convUid) {
-        contactImgSource = "data:image/png;base64," + UtilsAdapter.getContactImageString(
-                    accountId, convUid)
+        contactImage.updateImage(convUid)
         bestName = UtilsAdapter.getBestName(accountId, convUid)
 
         var id = UtilsAdapter.getBestId(accountId, convUid)
@@ -162,7 +160,7 @@ Rectangle {
                     ColumnLayout {
                         id: audioCallPageRectColumnLayout
 
-                        Image {
+                        AvatarImage {
                             id: contactImage
 
                             Layout.alignment: Qt.AlignCenter
@@ -170,9 +168,8 @@ Rectangle {
                             Layout.preferredWidth: 100
                             Layout.preferredHeight: 100
 
-                            fillMode: Image.PreserveAspectFit
-                            source: contactImgSource
-                            asynchronous: true
+                            mode: AvatarImage.Mode.FromConvUid
+                            showPresenceIndicator: false
                         }
 
                         Text {
diff --git a/src/mainview/components/ContactPicker.qml b/src/mainview/components/ContactPicker.qml
index 92e669ee3..11db11f9e 100644
--- a/src/mainview/components/ContactPicker.qml
+++ b/src/mainview/components/ContactPicker.qml
@@ -125,8 +125,6 @@ Popup {
     }
 
     onAboutToShow: {
-
-
         // Reset the model on each show.
         contactPickerListView.model = ContactAdapter.getContactSelectableModel(
                     type)
diff --git a/src/mainview/components/ContactPickerItemDelegate.qml b/src/mainview/components/ContactPickerItemDelegate.qml
index 531e97199..e9aa9b621 100644
--- a/src/mainview/components/ContactPickerItemDelegate.qml
+++ b/src/mainview/components/ContactPickerItemDelegate.qml
@@ -26,7 +26,7 @@ import "../../commoncomponents"
 ItemDelegate {
     id: contactPickerItemDelegate
 
-    Image {
+    AvatarImage {
         id: contactPickerContactImage
 
         anchors.left: parent.left
@@ -36,9 +36,8 @@ ItemDelegate {
         width: 40
         height: 40
 
-        fillMode: Image.PreserveAspectFit
-        source: "data:image/png;base64," + Picture
-        mipmap: true
+        mode: AvatarImage.Mode.FromContactUri
+        imageId: URI
     }
 
     Rectangle {
diff --git a/src/mainview/components/ConversationSmartListUserImage.qml b/src/mainview/components/ConversationSmartListUserImage.qml
deleted file mode 100644
index 772816122..000000000
--- a/src/mainview/components/ConversationSmartListUserImage.qml
+++ /dev/null
@@ -1,68 +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 QtQuick.Layouts 1.14
-import net.jami.Models 1.0
-import "../../commoncomponents"
-
-Image {
-    id: userImage
-
-    width: 40
-    height: 40
-
-    fillMode: Image.PreserveAspectFit
-    source: "data:image/png;base64," + Picture
-    mipmap: true
-
-    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
-
-            anchors.centerIn: unreadMessageCountRect
-
-            text: UnreadMessagesCount > 9 ? "···" : UnreadMessagesCount
-            color: "white"
-            font.pointSize: JamiTheme.textFontSize
-        }
-
-        radius: 30
-        color: JamiTheme.notificationRed
-    }
-}
diff --git a/src/mainview/components/ConversationSmartListView.qml b/src/mainview/components/ConversationSmartListView.qml
index 109f6918f..4c362da84 100644
--- a/src/mainview/components/ConversationSmartListView.qml
+++ b/src/mainview/components/ConversationSmartListView.qml
@@ -89,6 +89,8 @@ ListView {
 
     delegate: ConversationSmartListViewItemDelegate {
         id: smartListItemDelegate
+
+        onUpdateContactAvatarUidRequested: root.model.updateContactAvatarUid(uid)
     }
 
     ScrollIndicator.vertical: ScrollIndicator {}
diff --git a/src/mainview/components/ConversationSmartListViewItemDelegate.qml b/src/mainview/components/ConversationSmartListViewItemDelegate.qml
index f2489e5a4..a7f081976 100644
--- a/src/mainview/components/ConversationSmartListViewItemDelegate.qml
+++ b/src/mainview/components/ConversationSmartListViewItemDelegate.qml
@@ -30,6 +30,8 @@ ItemDelegate {
 
     property int lastInteractionPreferredWidth: 80
 
+    signal updateContactAvatarUidRequested(string uid)
+
     function convUid() {
         return UID
     }
@@ -76,14 +78,29 @@ ItemDelegate {
         }
     }
 
-    ConversationSmartListUserImage {
+    AvatarImage {
         id: conversationSmartListUserImage
 
         anchors.left: parent.left
         anchors.verticalCenter: parent.verticalCenter
         anchors.leftMargin: 16
-    }
 
+        width: 40
+        height: 40
+
+        mode: AvatarImage.Mode.FromContactUri
+
+        showPresenceIndicator: Presence === undefined ? false : Presence
+
+        unreadMessagesCount: UnreadMessagesCount
+
+        Component.onCompleted: {
+            var contactUid = URI
+            if (ContactType === Profile.Type.TEMPORARY)
+                updateContactAvatarUidRequested(contactUid)
+            updateImage(contactUid, PictureUid)
+        }
+    }
 
     RowLayout {
         id: rowUsernameAndLastInteractionDate
@@ -202,7 +219,7 @@ ItemDelegate {
                 userProfile.aliasText = DisplayName
                 userProfile.registeredNameText = DisplayID
                 userProfile.idText = URI
-                userProfile.contactPicBase64 = Picture
+                userProfile.contactImageUid = UID
                 smartListContextMenu.openMenu()
             } else if (mouse.button === Qt.LeftButton) {
                 conversationSmartListView.currentIndex = -1
diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml
index c98faa52b..e952bc524 100644
--- a/src/mainview/components/ParticipantOverlay.qml
+++ b/src/mainview/components/ParticipantOverlay.qml
@@ -39,6 +39,7 @@ Rectangle {
         participantName.text = name
     }
 
+    // TODO: try to use AvatarImage as well
     function setAvatar(avatar) {
         if (avatar === "") {
             opacity = 0
diff --git a/src/mainview/components/UserInfoCallPage.qml b/src/mainview/components/UserInfoCallPage.qml
index 2275bfd4b..98598f280 100644
--- a/src/mainview/components/UserInfoCallPage.qml
+++ b/src/mainview/components/UserInfoCallPage.qml
@@ -30,13 +30,11 @@ Rectangle {
     id: userInfoCallRect
 
     property int buttonPreferredSize: 48
-    property string contactImgSource: ""
     property string bestName: "Best Name"
     property string bestId: "Best Id"
 
     function updateUI(accountId, convUid) {
-        contactImgSource = "data:image/png;base64," + UtilsAdapter.getContactImageString(
-                    accountId, convUid)
+        contactImg.updateImage(convUid)
         bestName = UtilsAdapter.getBestName(accountId, convUid)
         var id = UtilsAdapter.getBestId(accountId, convUid)
         bestId = (bestName !== id) ? id : ""
@@ -74,7 +72,7 @@ Rectangle {
             onClicked: mainViewWindow.showWelcomeView()
         }
 
-        Image {
+        AvatarImage {
             id: contactImg
 
             Layout.alignment: Qt.AlignCenter
@@ -83,9 +81,8 @@ Rectangle {
             Layout.preferredWidth: 100
             Layout.preferredHeight: 100
 
-            fillMode: Image.PreserveAspectFit
-            source: contactImgSource
-            asynchronous: true
+            mode: AvatarImage.Mode.FromConvUid
+            showPresenceIndicator: false
         }
 
         Rectangle {
diff --git a/src/mainview/components/UserProfile.qml b/src/mainview/components/UserProfile.qml
index 4914d7eb0..31f6543cf 100644
--- a/src/mainview/components/UserProfile.qml
+++ b/src/mainview/components/UserProfile.qml
@@ -28,7 +28,7 @@ BaseDialog {
     id: root
 
     property string responsibleConvUid: ""
-    property string contactPicBase64: ""
+    property string contactImageUid: ""
     property string aliasText: ""
     property string registeredNameText: ""
     property string idText: ""
@@ -53,17 +53,17 @@ BaseDialog {
             rowSpacing: 16
             columnSpacing: 24
 
-            Image {
+            AvatarImage {
                 id: contactImage
 
                 Layout.alignment: Qt.AlignRight
-                Layout.preferredWidth: 130
+                Layout.preferredWidth: preferredImgSize
 
                 sourceSize.width: preferredImgSize
                 sourceSize.height: preferredImgSize
 
-                fillMode: Image.PreserveAspectFit
-                mipmap: true
+                mode: AvatarImage.Mode.FromConvUid
+                showPresenceIndicator: false
             }
 
             // Visible when user alias is not empty or equals to id.
@@ -196,8 +196,5 @@ BaseDialog {
             contactQrImage.source = "image://qrImage/contact_" + responsibleConvUid
     }
 
-    onContactPicBase64Changed: {
-        if (contactPicBase64 !== "")
-            contactImage.source = "data:image/png;base64," + contactPicBase64
-    }
+    onContactImageUidChanged: contactImage.updateImage(contactImageUid)
 }
diff --git a/src/messagesadapter.cpp b/src/messagesadapter.cpp
index f1e556b8a..da93bd504 100644
--- a/src/messagesadapter.cpp
+++ b/src/messagesadapter.cpp
@@ -450,14 +450,14 @@ MessagesAdapter::setConversationProfileData(const lrc::api::conversation::Info&
         auto& contact = accInfo->contactModel->getContact(contactUri);
         auto bestName = Utils::bestNameForConversation(convInfo, *convModel);
         setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING
-                      || contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY,
+                          || contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY,
                       bestName,
                       contactUri);
 
         if (!contact.profileInfo.avatar.isEmpty()) {
             setSenderImage(contactUri, contact.profileInfo.avatar);
         } else {
-            auto avatar = Utils::conversationPhoto(convInfo.uid, *accInfo, true);
+            auto avatar = Utils::contactPhoto(convInfo.participants[0]);
             QByteArray ba;
             QBuffer bu(&ba);
             avatar.save(&bu, "PNG");
diff --git a/src/pixbufmanipulator.cpp b/src/pixbufmanipulator.cpp
deleted file mode 100644
index 4860e7221..000000000
--- a/src/pixbufmanipulator.cpp
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2015-2020 by Savoir-faire Linux
- * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
- * Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
- * Author: Olivier Soldano <olivier.soldano@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/>.
- */
-
-#include "pixbufmanipulator.h"
-
-#include <QBuffer>
-#include <QByteArray>
-#include <QIODevice>
-#include <QImage>
-#include <QMetaType>
-#include <QPainter>
-#include <QSize>
-
-#include "globalinstances.h"
-
-#include <api/account.h>
-#include <api/contact.h>
-#include <api/contactmodel.h>
-#include <api/conversation.h>
-
-#include "utils.h"
-#undef interface
-
-QVariant
-PixbufManipulator::personPhoto(const QByteArray& data, const QString& type)
-{
-    QImage avatar;
-    const bool ret = avatar.loadFromData(QByteArray::fromBase64(data), type.toLatin1());
-    if (!ret) {
-        qDebug() << "vCard image loading failed";
-        return QVariant();
-    }
-    return QPixmap::fromImage(Utils::getCirclePhoto(avatar, avatar.size().width()));
-}
-
-QVariant
-PixbufManipulator::numberCategoryIcon(const QVariant& p,
-                                      const QSize& size,
-                                      bool displayPresence,
-                                      bool isPresent)
-{
-    Q_UNUSED(p)
-    Q_UNUSED(size)
-    Q_UNUSED(displayPresence)
-    Q_UNUSED(isPresent)
-    return QVariant();
-}
-
-QByteArray
-PixbufManipulator::toByteArray(const QVariant& pxm)
-{
-    auto image = pxm.value<QImage>();
-    QByteArray ba = Utils::QImageToByteArray(image);
-    return ba;
-}
-
-QVariant
-PixbufManipulator::userActionIcon(const UserActionElement& state) const
-{
-    Q_UNUSED(state)
-    return QVariant();
-}
-
-QVariant
-PixbufManipulator::decorationRole(const QModelIndex& index)
-{
-    Q_UNUSED(index)
-    return QVariant();
-}
-
-QVariant
-PixbufManipulator::decorationRole(const lrc::api::conversation::Info& conversationInfo,
-                                  const lrc::api::account::Info& accountInfo)
-{
-    QImage photo;
-    auto contacts = conversationInfo.participants;
-    if (contacts.empty()) {
-        return QVariant::fromValue(photo);
-    }
-    try {
-        /*
-         * Get first contact photo.
-         */
-        auto contactUri = contacts.front();
-        auto contactInfo = accountInfo.contactModel->getContact(contactUri);
-        auto contactPhoto = contactInfo.profileInfo.avatar;
-        auto bestName = Utils::bestNameForContact(contactInfo);
-        auto bestId = Utils::bestIdForContact(contactInfo);
-        if (accountInfo.profileInfo.type == lrc::api::profile::Type::SIP
-            && contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY) {
-            photo = Utils::fallbackAvatar(QString(), QString());
-        } else if (contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY
-                   && contactInfo.profileInfo.uri.isEmpty()) {
-            photo = Utils::fallbackAvatar(QString(), QString());
-        } else if (!contactPhoto.isEmpty()) {
-            QByteArray byteArray = contactPhoto.toLocal8Bit();
-            photo = personPhoto(byteArray, nullptr).value<QImage>();
-            if (photo.isNull()) {
-                auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
-                photo = Utils::fallbackAvatar("ring:" + contactInfo.profileInfo.uri, avatarName);
-            }
-        } else {
-            auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
-            photo = Utils::fallbackAvatar("ring:" + contactInfo.profileInfo.uri, avatarName);
-        }
-    } catch (...) {
-    }
-    return QVariant::fromValue(Utils::scaleAndFrame(photo));
-}
diff --git a/src/pixbufmanipulator.h b/src/pixbufmanipulator.h
deleted file mode 100644
index 7788f6526..000000000
--- a/src/pixbufmanipulator.h
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2015-2020 by Savoir-faire Linux
- * Author: Edric Ladent Milaret <edric.ladent-milaret@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 <QImage>
-
-#include <interfaces/pixmapmanipulatori.h>
-#include <memory>
-
-Q_DECLARE_METATYPE(QImage);
-
-class Person;
-
-QByteArray QImageToByteArray(QImage image);
-
-class PixbufManipulator : public Interfaces::PixmapManipulatorI
-{
-public:
-    QVariant personPhoto(const QByteArray& data, const QString& type = "PNG") override;
-
-    /*
-     * TODO: the following methods return an empty QVariant/QByteArray.
-     */
-    QVariant numberCategoryIcon(const QVariant& p,
-                                const QSize& size,
-                                bool displayPresence = false,
-                                bool isPresent = false) override;
-    QByteArray toByteArray(const QVariant& pxm) override;
-    QVariant userActionIcon(const UserActionElement& state) const override;
-    QVariant decorationRole(const QModelIndex& index) override;
-    QVariant decorationRole(const lrc::api::conversation::Info& conversation,
-                            const lrc::api::account::Info& accountInfo) override;
-};
diff --git a/src/previewrenderer.cpp b/src/previewrenderer.cpp
index d5c72a14c..92cd0f0cb 100644
--- a/src/previewrenderer.cpp
+++ b/src/previewrenderer.cpp
@@ -112,24 +112,6 @@ PhotoboothPreviewRender::PhotoboothPreviewRender(QQuickItem* parent)
 
 PhotoboothPreviewRender::~PhotoboothPreviewRender() {}
 
-QImage
-PhotoboothPreviewRender::takePhoto()
-{
-    if (auto previewImage = LRCInstance::renderer()->getPreviewFrame()) {
-        return previewImage->copy();
-    }
-    return QImage();
-}
-
-QString
-PhotoboothPreviewRender::takeCroppedPhotoToBase64(int size)
-{
-    auto image = Utils::cropImage(takePhoto());
-    auto avatar = image.scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
-
-    return QString::fromLatin1(Utils::QImageToByteArray(avatar).toBase64().data());
-}
-
 void
 PhotoboothPreviewRender::paint(QPainter* painter)
 {
diff --git a/src/previewrenderer.h b/src/previewrenderer.h
index 6a2a66868..55249b124 100644
--- a/src/previewrenderer.h
+++ b/src/previewrenderer.h
@@ -63,9 +63,6 @@ public:
     explicit PhotoboothPreviewRender(QQuickItem* parent = 0);
     virtual ~PhotoboothPreviewRender();
 
-    QImage takePhoto();
-    Q_INVOKABLE QString takeCroppedPhotoToBase64(int size);
-
 signals:
     void hideBooth();
 
diff --git a/src/settingsadapter.cpp b/src/settingsadapter.cpp
index 8b6213ac7..a704512f8 100644
--- a/src/settingsadapter.cpp
+++ b/src/settingsadapter.cpp
@@ -263,15 +263,6 @@ SettingsAdapter::getAccountBestName()
     return Utils::bestNameForAccount(LRCInstance::getCurrentAccountInfo());
 }
 
-QString
-SettingsAdapter::getAvatarImage_Base64(int avatarSize)
-{
-    auto& accountInfo = LRCInstance::getCurrentAccountInfo();
-    auto avatar = Utils::accountPhoto(accountInfo, {avatarSize, avatarSize});
-
-    return QString::fromLatin1(Utils::QImageToByteArray(avatar).toBase64().data());
-}
-
 bool
 SettingsAdapter::getIsDefaultAvatar()
 {
@@ -280,18 +271,10 @@ SettingsAdapter::getIsDefaultAvatar()
     return accountInfo.profileInfo.avatar.isEmpty();
 }
 
-bool
-SettingsAdapter::setCurrAccAvatar(QString avatarImgBase64)
-{
-    QImage avatarImg;
-    const bool ret = avatarImg.loadFromData(QByteArray::fromBase64(avatarImgBase64.toLatin1()));
-    if (!ret) {
-        qDebug() << "Current avatar loading from base64 fail";
-        return false;
-    } else {
-        LRCInstance::setCurrAccAvatar(QPixmap::fromImage(avatarImg));
-    }
-    return true;
+void
+SettingsAdapter::setCurrAccAvatar(QVariant avatarImg)
+{
+    LRCInstance::setCurrAccAvatar(QPixmap::fromImage(avatarImg.value<QImage>()));
 }
 
 void
diff --git a/src/settingsadapter.h b/src/settingsadapter.h
index 78a5164ff..db621f598 100644
--- a/src/settingsadapter.h
+++ b/src/settingsadapter.h
@@ -94,9 +94,8 @@ public:
     Q_INVOKABLE QString getAccountBestName();
 
     // getters and setters of avatar image
-    Q_INVOKABLE QString getAvatarImage_Base64(int avatarSize);
     Q_INVOKABLE bool getIsDefaultAvatar();
-    Q_INVOKABLE bool setCurrAccAvatar(QString avatarImgBase64);
+    Q_INVOKABLE void setCurrAccAvatar(QVariant avatarImg);
     Q_INVOKABLE void clearCurrentAvatar();
 
     /*
diff --git a/src/settingsview/components/AccountProfile.qml b/src/settingsview/components/AccountProfile.qml
index 0f8963709..41a8719e4 100644
--- a/src/settingsview/components/AccountProfile.qml
+++ b/src/settingsview/components/AccountProfile.qml
@@ -52,7 +52,7 @@ ColumnLayout {
     }
 
     function setAvatar() {
-        currentAccountAvatar.setAvatarPixmap(SettingsAdapter.getAvatarImage_Base64(currentAccountAvatar.boothWidth), SettingsAdapter.getIsDefaultAvatar())
+        currentAccountAvatar.setAvatarImage()
     }
 
     function stopBooth() {
@@ -79,14 +79,8 @@ ColumnLayout {
         Layout.fillWidth: true
         Layout.alignment: Qt.AlignCenter
 
+        saveToConfig: true
         boothWidth: 180
-
-        onImageAcquired: SettingsAdapter.setCurrAccAvatar(imgBase64)
-
-        onImageCleared: {
-            SettingsAdapter.clearCurrentAvatar()
-            setAvatar()
-        }
     }
 
     MaterialLineEdit {
diff --git a/src/settingsview/components/BannedContacts.qml b/src/settingsview/components/BannedContacts.qml
index 6bce03f14..f40592f6f 100644
--- a/src/settingsview/components/BannedContacts.qml
+++ b/src/settingsview/components/BannedContacts.qml
@@ -137,7 +137,6 @@ ColumnLayout {
 
             contactName : ContactName
             contactID: ContactID
-            contactPicture_base64: ContactPicture
 
             onClicked: bannedListWidget.currentIndex = index
 
diff --git a/src/settingsview/components/BannedItemDelegate.qml b/src/settingsview/components/BannedItemDelegate.qml
index ca93d18fb..e20dc9215 100644
--- a/src/settingsview/components/BannedItemDelegate.qml
+++ b/src/settingsview/components/BannedItemDelegate.qml
@@ -31,12 +31,13 @@ ItemDelegate {
 
     property string contactName : ""
     property string contactID: ""
-    property string contactPicture_base64:""
 
     signal btnReAddContactClicked
 
     highlighted: ListView.isCurrentItem
 
+    onContactIDChanged: avatarImg.updateImage(contactID)
+
     RowLayout {
         anchors.fill: parent
 
@@ -52,11 +53,14 @@ ItemDelegate {
             background: Rectangle {
                 anchors.fill: parent
                 color: "transparent"
-                Image {
+                AvatarImage {
                     id: avatarImg
 
                     anchors.fill: parent
-                    source: contactPicture_base64 === "" ? "" : "data:image/png;base64," + contactPicture_base64
+
+                    mode: AvatarImage.Mode.FromContactUri
+                    showPresenceIndicator: false
+
                     fillMode: Image.PreserveAspectCrop
                     layer.enabled: true
                     layer.effect: OpacityMask {
diff --git a/src/smartlistmodel.cpp b/src/smartlistmodel.cpp
index 3fd0b47f2..439a6c1f8 100644
--- a/src/smartlistmodel.cpp
+++ b/src/smartlistmodel.cpp
@@ -21,12 +21,8 @@
 #include "smartlistmodel.h"
 
 #include "lrcinstance.h"
-#include "pixbufmanipulator.h"
 #include "utils.h"
 
-#include "api/contactmodel.h"
-#include "globalinstances.h"
-
 #include <QDateTime>
 
 SmartListModel::SmartListModel(QObject* parent,
@@ -148,7 +144,6 @@ SmartListModel::roleNames() const
     QHash<int, QByteArray> roles;
     roles[DisplayName] = "DisplayName";
     roles[DisplayID] = "DisplayID";
-    roles[Picture] = "Picture";
     roles[Presence] = "Presence";
     roles[URI] = "URI";
     roles[UnreadMessagesCount] = "UnreadMessagesCount";
@@ -163,6 +158,7 @@ SmartListModel::roleNames() const
     roles[SectionName] = "SectionName";
     roles[AccountId] = "AccountId";
     roles[Draft] = "Draft";
+    roles[PictureUid] = "PictureUid";
     return roles;
 }
 
@@ -183,6 +179,8 @@ void
 SmartListModel::fillConversationsList()
 {
     beginResetModel();
+    fillContactAvatarUidMap(LRCInstance::getCurrentAccountInfo().contactModel->getAllContacts());
+
     auto* convModel = LRCInstance::getCurrentConversationModel();
     conversations_.clear();
 
@@ -208,6 +206,39 @@ SmartListModel::updateConversation(const QString& convUid)
     }
 }
 
+void
+SmartListModel::updateContactAvatarUid(const QString& contactUri)
+{
+    contactAvatarUidMap_[contactUri] = Utils::generateUid();
+}
+
+void
+SmartListModel::fillContactAvatarUidMap(const 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));
+        }
+    }
+}
+
 void
 SmartListModel::toggleSection(const QString& section)
 {
@@ -241,12 +272,10 @@ SmartListModel::getConversationItemData(const conversation::Info& item,
         return QVariant();
     }
     auto& contactModel = accountInfo.contactModel;
+
+    // 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::Picture: {
-        auto contactImage
-            = GlobalInstances::pixmapManipulator().decorationRole(item, accountInfo).value<QImage>();
-        return QString::fromLatin1(Utils::QImageToByteArray(contactImage).toBase64().data());
-    }
     case Role::DisplayName: {
         if (!item.participants.isEmpty()) {
             auto& contact = contactModel->getContact(item.participants[0]);
@@ -268,10 +297,15 @@ SmartListModel::getConversationItemData(const conversation::Info& item,
         }
         return QVariant(false);
     }
+    case Role::PictureUid: {
+        if (!item.participants.isEmpty()) {
+            return QVariant(contactAvatarUidMap_[item.participants[0]]);
+        }
+        return QVariant("");
+    }
     case Role::URI: {
         if (!item.participants.isEmpty()) {
-            auto& contact = contactModel->getContact(item.participants[0]);
-            return QVariant(contact.profileInfo.uri);
+            return QVariant(item.participants[0]);
         }
         return QVariant("");
     }
@@ -331,13 +365,13 @@ SmartListModel::getConversationItemData(const conversation::Info& item,
         if (!convInfo.uid.isEmpty()) {
             auto* callModel = LRCInstance::getCurrentCallModel();
             const auto call = callModel->getCall(convInfo.callId);
-            return QVariant(callModel->hasCall(convInfo.callId)
-                            && ((!call.isOutgoing
-                                 && (call.status == lrc::api::call::Status::IN_PROGRESS
-                                     || call.status == lrc::api::call::Status::PAUSED
-                                     || call.status == lrc::api::call::Status::INCOMING_RINGING))
-                                || (call.isOutgoing
-                                    && call.status != lrc::api::call::Status::ENDED)));
+            return QVariant(
+                callModel->hasCall(convInfo.callId)
+                && ((!call.isOutgoing
+                     && (call.status == lrc::api::call::Status::IN_PROGRESS
+                         || call.status == lrc::api::call::Status::PAUSED
+                         || call.status == lrc::api::call::Status::INCOMING_RINGING))
+                    || (call.isOutgoing && call.status != lrc::api::call::Status::ENDED)));
         }
         return QVariant(false);
     }
diff --git a/src/smartlistmodel.h b/src/smartlistmodel.h
index 9bb56900c..ccf7a0fea 100644
--- a/src/smartlistmodel.h
+++ b/src/smartlistmodel.h
@@ -24,6 +24,7 @@
 #include "api/contact.h"
 #include "api/conversation.h"
 #include "api/conversationmodel.h"
+#include "api/contactmodel.h"
 
 #include <QAbstractItemModel>
 
@@ -42,7 +43,6 @@ public:
     enum Role {
         DisplayName = Qt::UserRole + 1,
         DisplayID,
-        Picture,
         Presence,
         URI,
         UnreadMessagesCount,
@@ -58,6 +58,7 @@ public:
         CallState,
         SectionName,
         AccountId,
+        PictureUid,
         Draft
     };
     Q_ENUM(Role)
@@ -85,15 +86,28 @@ public:
     Q_INVOKABLE void fillConversationsList();
     Q_INVOKABLE void updateConversation(const QString& conv);
 
+    /*
+     * This function is to update contact avatar uuid for current account when there's an contact
+     * avatar changed.
+     */
+    Q_INVOKABLE void updateContactAvatarUid(const QString& contactUri);
+
 private:
     QVariant getConversationItemData(const ConversationInfo& item,
                                      const AccountInfo& accountInfo,
                                      int role) const;
+
+    /*
+     * Give a uuid for each contact avatar for current account and it will serve PictureUid role
+     */
+    void fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts);
+
     /*
      * List sectioning.
      */
     Type listModelType_;
     QMap<QString, bool> sectionState_;
     QMap<ConferenceableItem, ConferenceableValue> conferenceables_;
+    QMap<QString, QString> contactAvatarUidMap_;
     ConversationModel::ConversationQueue conversations_;
 };
diff --git a/src/utils.cpp b/src/utils.cpp
index b215d2360..b2d066418 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -25,9 +25,7 @@
 #include "globalsystemtray.h"
 #include "jamiavatartheme.h"
 #include "lrcinstance.h"
-#include "pixbufmanipulator.h"
 
-#include <globalinstances.h>
 #include <qrencode.h>
 
 #include <QApplication>
@@ -43,6 +41,7 @@
 #include <QSvgRenderer>
 #include <QTranslator>
 #include <QtConcurrent/QtConcurrent>
+#include <QUuid>
 
 #ifdef Q_OS_WIN
 #include <lmcons.h>
@@ -245,14 +244,52 @@ Utils::GetISODate()
 #endif
 }
 
-QString
-Utils::getContactImageString(const QString& accountId, const QString& uid)
+QImage
+Utils::contactPhoto(const QString& contactUri, const QSize& size)
+{
+    QImage photo;
+
+    try {
+        /*
+         * Get first contact photo.
+         */
+        auto& accountInfo = LRCInstance::accountModel().getAccountInfo(LRCInstance::getCurrAccId());
+        auto contactInfo = accountInfo.contactModel->getContact(contactUri);
+        auto contactPhoto = contactInfo.profileInfo.avatar;
+        auto bestName = Utils::bestNameForContact(contactInfo);
+        auto bestId = Utils::bestIdForContact(contactInfo);
+        if (accountInfo.profileInfo.type == lrc::api::profile::Type::SIP
+            && contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY) {
+            photo = Utils::fallbackAvatar(QString(), QString());
+        } else if (contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY
+                   && contactInfo.profileInfo.uri.isEmpty()) {
+            photo = Utils::fallbackAvatar(QString(), QString());
+        } else if (!contactPhoto.isEmpty()) {
+            QByteArray byteArray = contactPhoto.toLocal8Bit();
+            photo = contactPhotoFromBase64(byteArray, nullptr);
+            if (photo.isNull()) {
+                auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
+                photo = Utils::fallbackAvatar("ring:" + contactInfo.profileInfo.uri, avatarName);
+            }
+        } else {
+            auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
+            photo = Utils::fallbackAvatar("ring:" + contactInfo.profileInfo.uri, avatarName);
+        }
+    } catch (...) {
+    }
+    return Utils::scaleAndFrame(photo, size);
+}
+
+QImage
+Utils::contactPhotoFromBase64(const QByteArray& data, const QString& type)
 {
-    return QString::fromLatin1(
-        Utils::QImageToByteArray(
-            Utils::conversationPhoto(uid, LRCInstance::getAccountInfo(accountId)))
-            .toBase64()
-            .data());
+    QImage avatar;
+    const bool ret = avatar.loadFromData(QByteArray::fromBase64(data), type.toLatin1());
+    if (!ret) {
+        qDebug() << "Utils: vCard image loading failed";
+        return QImage();
+    }
+    return Utils::getCirclePhoto(avatar, avatar.size().width());
 }
 
 QImage
@@ -549,21 +586,6 @@ Utils::getReplyMessageBox(QWidget* widget, const QString& title, const QString&
     return false;
 }
 
-QImage
-Utils::conversationPhoto(const QString& convUid,
-                         const lrc::api::account::Info& accountInfo,
-                         bool filtered)
-{
-    auto* convModel = LRCInstance::getCurrentConversationModel();
-    const auto convInfo = convModel->getConversationForUID(convUid);
-    if (!convInfo.uid.isEmpty()) {
-        return GlobalInstances::pixmapManipulator()
-            .decorationRole(convInfo, accountInfo)
-            .value<QImage>();
-    }
-    return QImage();
-}
-
 QColor
 Utils::getAvatarColor(const QString& canonicalUri)
 {
@@ -587,10 +609,12 @@ Utils::getAvatarColor(const QString& canonicalUri)
 QImage
 Utils::fallbackAvatar(const QString& canonicalUriStr, const QString& letterStr, const QSize& size)
 {
+    auto sizeToUse = size.height() >= defaultAvatarSize.height() ? size : defaultAvatarSize;
+
     /*
      * We start with a transparent avatar.
      */
-    QImage avatar(size, QImage::Format_ARGB32);
+    QImage avatar(sizeToUse, QImage::Format_ARGB32);
     avatar.fill(Qt::transparent);
 
     /*
@@ -651,7 +675,7 @@ Utils::fallbackAvatar(const QString& canonicalUriStr, const QString& letterStr,
         painter.drawPixmap(overlayRect, QPixmap(":/images/default_avatar_overlay.svg"));
     }
 
-    return avatar;
+    return avatar.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
 }
 
 QImage
@@ -802,7 +826,7 @@ Utils::accountPhoto(const lrc::api::account::Info& accountInfo, const QSize& siz
     QImage photo;
     if (!accountInfo.profileInfo.avatar.isEmpty()) {
         QByteArray ba = accountInfo.profileInfo.avatar.toLocal8Bit();
-        photo = GlobalInstances::pixmapManipulator().personPhoto(ba, nullptr).value<QImage>();
+        photo = contactPhotoFromBase64(ba, nullptr);
     } else {
         auto bestId = bestIdForAccount(accountInfo);
         auto bestName = bestNameForAccount(accountInfo);
@@ -843,3 +867,9 @@ Utils::isImage(const QString& fileExt)
         return true;
     return false;
 }
+
+QString
+Utils::generateUid()
+{
+    return QUuid::createUuid().toString();
+}
diff --git a/src/utils.h b/src/utils.h
index ccecb8752..2e74d0315 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -100,11 +100,9 @@ bool getReplyMessageBox(QWidget* widget, const QString& title, const QString& te
  * Image manipulation
  */
 static const QSize defaultAvatarSize {128, 128};
-QString getContactImageString(const QString& accountId, const QString& uid);
+QImage contactPhotoFromBase64(const QByteArray& data, const QString& type);
+QImage contactPhoto(const QString& contactUri, const QSize& size = defaultAvatarSize);
 QImage getCirclePhoto(const QImage original, int sizePhoto);
-QImage conversationPhoto(const QString& convUid,
-                         const lrc::api::account::Info& accountInfo,
-                         bool filtered = false);
 QColor getAvatarColor(const QString& canonicalUri);
 QImage fallbackAvatar(const QString& canonicalUriStr,
                       const QString& letterStr = QString(),
@@ -123,6 +121,7 @@ QImage cropImage(const QImage& img);
 QPixmap pixmapFromSvg(const QString& svg_resource, const QSize& size);
 QImage setupQRCode(QString ringID, int margin);
 bool isImage(const QString& fileExt);
+QString generateUid();
 
 /*
  * Misc
diff --git a/src/utilsadapter.cpp b/src/utilsadapter.cpp
index d69d7049d..137d4dd5e 100644
--- a/src/utilsadapter.cpp
+++ b/src/utilsadapter.cpp
@@ -105,12 +105,6 @@ UtilsAdapter::checkStartupLink()
     return Utils::CheckStartupLink(L"Jami");
 }
 
-const QString
-UtilsAdapter::getContactImageString(const QString& accountId, const QString& uid)
-{
-    return Utils::getContactImageString(accountId, uid);
-}
-
 const QString
 UtilsAdapter::getBestName(const QString& accountId, const QString& uid)
 {
@@ -356,17 +350,6 @@ UtilsAdapter::getAbsPath(QString path)
 #endif
 }
 
-QString
-UtilsAdapter::getCroppedImageBase64FromFile(QString fileName, int size)
-{
-    auto image = Utils::cropImage(QImage(fileName));
-    auto croppedImage = image.scaled(size,
-                                     size,
-                                     Qt::KeepAspectRatioByExpanding,
-                                     Qt::SmoothTransformation);
-    return QString::fromLatin1(Utils::QImageToByteArray(croppedImage).toBase64().data());
-}
-
 bool
 UtilsAdapter::checkShowPluginsButton()
 {
diff --git a/src/utilsadapter.h b/src/utilsadapter.h
index 9a30e12cf..36e0658c7 100644
--- a/src/utilsadapter.h
+++ b/src/utilsadapter.h
@@ -44,7 +44,6 @@ public:
     Q_INVOKABLE bool createStartupLink();
     Q_INVOKABLE QString GetRingtonePath();
     Q_INVOKABLE bool checkStartupLink();
-    Q_INVOKABLE const QString getContactImageString(const QString& accountId, const QString& uid);
     Q_INVOKABLE void removeConversation(const QString& accountId,
                                         const QString& uid,
                                         bool banContact = false);
@@ -77,7 +76,6 @@ public:
     Q_INVOKABLE QString toFileInfoName(QString inputFileName);
     Q_INVOKABLE QString toFileAbsolutepath(QString inputFileName);
     Q_INVOKABLE QString getAbsPath(QString path);
-    Q_INVOKABLE QString getCroppedImageBase64FromFile(QString fileName, int size);
     Q_INVOKABLE bool checkShowPluginsButton();
     Q_INVOKABLE QString fileName(const QString& path);
     Q_INVOKABLE QString getExt(const QString& path);
diff --git a/src/wizardview/WizardView.qml b/src/wizardview/WizardView.qml
index 154b7bd3a..e836c060d 100644
--- a/src/wizardview/WizardView.qml
+++ b/src/wizardview/WizardView.qml
@@ -385,14 +385,13 @@ Rectangle {
                 }
 
                 onSaveProfile: {
-                    SettingsAdapter.setCurrAccAvatar(profilePage.boothImgBase64)
+                    if (profilePage.profileImg)
+                        SettingsAdapter.setCurrAccAvatar(profilePage.profileImg)
                     AccountAdapter.setCurrAccDisplayName(profilePage.displayName)
                     leave()
                 }
 
-                onLeavePage: {
-                    leave()
-                }
+                onLeavePage: leave()
             }
         }
     }
diff --git a/src/wizardview/components/CreateSIPAccountPage.qml b/src/wizardview/components/CreateSIPAccountPage.qml
index 2e7f8570c..f077aebfe 100644
--- a/src/wizardview/components/CreateSIPAccountPage.qml
+++ b/src/wizardview/components/CreateSIPAccountPage.qml
@@ -32,8 +32,6 @@ Rectangle {
     property alias text_sipPasswordEditAlias: sipPasswordEdit.text
     property int preferredHeight: createSIPAccountPageColumnLayout.implicitHeight
 
-    property var boothImgBase64: null
-
     signal createAccount
     signal leavePage
 
diff --git a/src/wizardview/components/ProfilePage.qml b/src/wizardview/components/ProfilePage.qml
index 612b4f460..adc7ac5d3 100644
--- a/src/wizardview/components/ProfilePage.qml
+++ b/src/wizardview/components/ProfilePage.qml
@@ -26,11 +26,13 @@ import "../../commoncomponents"
 Rectangle {
     id: root
 
+    property alias profileImg: setAvatarWidget.boothImg
     property int preferredHeight: profilePageColumnLayout.implicitHeight
 
     function initializeOnShowUp() {
+        setAvatarWidget.hasAvatar = false
+        setAvatarWidget.setAvatarImage(AvatarImage.Mode.Default, "")
         clearAllTextFields()
-        boothImgBase64 = ""
         saveProfileBtn.spinnerTriggered = true
     }
 
@@ -48,7 +50,6 @@ Rectangle {
     signal saveProfile
 
     property var showBottom: false
-    property alias boothImgBase64: setAvatarWidget.imgBase64
     property alias displayName: aliasEdit.text
     property bool isRdv: false
 
-- 
GitLab