From 4bda3306374e80a8ccda84127839551ac8a32228 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Tue, 29 Jun 2021 10:39:06 -0400
Subject: [PATCH] swarm: simplify and update avatar update mechanism

Implements a leaner avatar caching system. The avatar component
listens for uid filtering its id, which may be:
- conversation id
- account id
- contact uri

In response to the uid change, a the image source is updated with
a new image url invoking a fresh QQuickImageProvider query. With
this design, only the avatarregistry's uid mapping needs to be
updated when profiles are changed, and no longer should specific
avatar components receive manual source updates.

Gitlab: #466
Change-Id: Ie5313f5c187a0977ca51b890dd92187480a42537
---
 CMakeLists.txt                                |   6 +-
 qml.qrc                                       |   3 +-
 src/accountadapter.cpp                        |  33 ++-
 src/accountadapter.h                          |   3 +-
 src/accountlistmodel.cpp                      |  27 --
 src/accountlistmodel.h                        |  20 +-
 src/avatarimageprovider.h                     |  60 ++--
 src/avatarregistry.cpp                        |  84 ++++++
 src/avatarregistry.h                          |  51 ++++
 src/calloverlaymodel.cpp                      |   2 -
 .../AccountMigrationDialog.qml                |  17 +-
 src/commoncomponents/Avatar.qml               | 118 ++++++++
 src/commoncomponents/AvatarImage.qml          | 229 ----------------
 src/commoncomponents/PhotoboothView.qml       | 258 ++++++------------
 src/commoncomponents/SpinningAnimation.qml    |  20 +-
 src/constant/JamiStrings.qml                  |   3 +-
 src/constant/JamiTheme.qml                    |   1 +
 src/conversationlistmodelbase.cpp             |  36 ---
 src/conversationlistmodelbase.h               |  10 -
 src/conversationsadapter.cpp                  |  10 +-
 src/lrcinstance.cpp                           |  28 --
 src/lrcinstance.h                             |   3 -
 src/mainapplication.cpp                       |   4 +
 src/mainview/MainView.qml                     |   8 -
 src/mainview/components/AccountComboBox.qml   |  19 +-
 .../components/AccountItemDelegate.qml        |   5 +-
 .../components/ContactPickerItemDelegate.qml  |  15 +-
 .../components/ConversationAvatar.qml         |  49 ++++
 .../components/ConversationListView.qml       |   2 +-
 src/mainview/components/InitialCallPage.qml   |  11 +-
 src/mainview/components/OngoingCallPage.qml   |   5 +-
 .../ParticipantCallInStatusDelegate.qml       |   6 +-
 .../components/ParticipantOverlay.qml         |  30 +-
 .../components/SmartListItemDelegate.qml      |  30 +-
 src/mainview/components/UserProfile.qml       |  11 +-
 src/previewrenderer.cpp                       |  13 +-
 src/previewrenderer.h                         |  10 +-
 src/quickimageproviderbase.h                  |   4 +-
 src/searchresultslistmodel.cpp                |   1 -
 src/selectablelistproxymodel.cpp              |   8 -
 src/selectablelistproxymodel.h                |   4 -
 .../components/AccountProfile.qml             |  10 +-
 .../components/ContactItemDelegate.qml        |  33 +--
 .../components/CurrentAccountSettings.qml     |   2 -
 src/smartlistmodel.cpp                        |   2 -
 src/utils.cpp                                 | 168 ++++++------
 src/utils.h                                   |  17 +-
 src/wizardview/WizardView.qml                 |   1 -
 src/wizardview/components/ProfilePage.qml     |  16 +-
 49 files changed, 632 insertions(+), 874 deletions(-)
 create mode 100644 src/avatarregistry.cpp
 create mode 100644 src/avatarregistry.h
 create mode 100644 src/commoncomponents/Avatar.qml
 delete mode 100644 src/commoncomponents/AvatarImage.qml
 create mode 100644 src/mainview/components/ConversationAvatar.qml

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