From ec0feef74d1da0de950321cc281c2fb092a5461e Mon Sep 17 00:00:00 2001
From: Matheo Joseph <matheo.joseph@savoirfairelinux.com>
Date: Mon, 26 Jun 2023 17:12:01 -0400
Subject: [PATCH] CachedImage: add icon downloader for pluginstore and welcome
 page

Gitlab: #1228
Gitlab: #1166

Change-Id: I0117cecdb8a77ded8f3da3c2f25028012002a285
---
 CMakeLists.txt                              |   7 +-
 src/app/MainApplicationWindow.qml           |   2 +-
 src/app/imagedownloader.cpp                 |  66 ++++++++++
 src/app/imagedownloader.h                   |  42 +++++++
 src/app/mainapplication.cpp                 |   4 +-
 src/app/mainapplication.h                   |   4 +-
 src/app/mainview/components/CachedImage.qml | 132 ++++++++++++++++++++
 src/app/qmlregister.cpp                     |   9 +-
 src/app/qmlregister.h                       |   3 +-
 src/app/utilsadapter.cpp                    |  67 +++++++++-
 src/app/utilsadapter.h                      |   9 ++
 src/app/webengine/GeneralWebEngineView.qml  |   4 +-
 tests/qml/main.cpp                          |   2 +-
 tests/qml/src/tst_CachedImage.qml           |  86 +++++++++++++
 14 files changed, 421 insertions(+), 16 deletions(-)
 create mode 100644 src/app/imagedownloader.cpp
 create mode 100644 src/app/imagedownloader.h
 create mode 100644 src/app/mainview/components/CachedImage.qml
 create mode 100644 tests/qml/src/tst_CachedImage.qml

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5f06f2cbf..4f936b48d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -238,7 +238,8 @@ set(COMMON_SOURCES
   ${APP_SRC_DIR}/positioning.cpp
   ${APP_SRC_DIR}/currentcall.cpp
   ${APP_SRC_DIR}/messageparser.cpp
-  ${APP_SRC_DIR}/previewengine.cpp)
+  ${APP_SRC_DIR}/previewengine.cpp
+  ${APP_SRC_DIR}/imagedownloader.cpp)
 
 set(COMMON_HEADERS
   ${APP_SRC_DIR}/avatarimageprovider.h
@@ -301,7 +302,9 @@ set(COMMON_HEADERS
   ${APP_SRC_DIR}/positioning.h
   ${APP_SRC_DIR}/currentcall.h
   ${APP_SRC_DIR}/messageparser.h
-  ${APP_SRC_DIR}/htmlparser.h)
+  ${APP_SRC_DIR}/htmlparser.h
+  ${APP_SRC_DIR}/imagedownloader.h)
+
 
 # For libavutil/avframe.
 set(LIBJAMI_CONTRIB_DIR "${DAEMON_DIR}/contrib")
diff --git a/src/app/MainApplicationWindow.qml b/src/app/MainApplicationWindow.qml
index 5da6ea9cb..6410f6a54 100644
--- a/src/app/MainApplicationWindow.qml
+++ b/src/app/MainApplicationWindow.qml
@@ -270,7 +270,7 @@ ApplicationWindow {
     Connections {
         target: UpdateManager
 
-        function onUpdateDownloadStarted() {
+        function onDownloadStarted() {
             viewCoordinator.presentDialog(appWindow, "settingsview/components/UpdateDownloadDialog.qml", {
                     "title": JamiStrings.updateDialogTitle
                 });
diff --git a/src/app/imagedownloader.cpp b/src/app/imagedownloader.cpp
new file mode 100644
index 000000000..945a4e607
--- /dev/null
+++ b/src/app/imagedownloader.cpp
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 Savoir-faire Linux Inc.
+ *
+ * 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 "imagedownloader.h"
+#include <QDir>
+#include <QLockFile>
+
+ImageDownloader::ImageDownloader(ConnectivityMonitor* cm, QObject* parent)
+    : NetworkManager(cm, parent)
+{}
+
+void
+ImageDownloader::downloadImage(const QUrl& url, const QString& localPath)
+{
+    Utils::oneShotConnect(this, &NetworkManager::errorOccured, this, [this, localPath]() {
+        onDownloadImageFinished({}, localPath);
+    });
+
+    sendGetRequest(url, [this, localPath](const QByteArray& imageData) {
+        onDownloadImageFinished(imageData, localPath);
+    });
+}
+
+void
+ImageDownloader::onDownloadImageFinished(const QByteArray& data, const QString& localPath)
+{
+    if (!data.isEmpty()) {
+        // Check if the parent folders exist create them if not
+        QString dirPath = localPath.left(localPath.lastIndexOf('/'));
+        QDir dir;
+        dir.mkpath(dirPath);
+
+        QLockFile lf(localPath + ".lock");
+        QFile file(localPath);
+
+        if (!lf.lock()) {
+            qWarning().noquote() << "Can't lock file for writing: " << file.fileName();
+            return;
+        }
+        if (!file.open(QIODevice::WriteOnly)) {
+            qWarning().noquote() << "Can't open file for writing: " << file.fileName();
+            return;
+        }
+
+        file.write(data);
+        file.close();
+        qWarning() << Q_FUNC_INFO;
+        Q_EMIT downloadImageSuccessful(localPath);
+        return;
+    }
+    Q_EMIT downloadImageFailed(localPath);
+}
diff --git a/src/app/imagedownloader.h b/src/app/imagedownloader.h
new file mode 100644
index 000000000..ed8c1b129
--- /dev/null
+++ b/src/app/imagedownloader.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 Savoir-faire Linux Inc.
+ *
+ * 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 "networkmanager.h"
+#include "qtutils.h"
+
+class ImageDownloader : public NetworkManager
+{
+    Q_OBJECT
+
+    QML_PROPERTY(QString, cachePath)
+
+public:
+    explicit ImageDownloader(ConnectivityMonitor* cm, QObject* parent = nullptr);
+
+    // Download an image and call onDownloadImageFinished when done
+    Q_INVOKABLE void downloadImage(const QUrl& url, const QString& localPath);
+
+Q_SIGNALS:
+    void downloadImageSuccessful(const QString& localPath);
+    void downloadImageFailed(const QString& localPath);
+
+private Q_SLOTS:
+    // Saves the image to the localPath and emits the appropriate signal
+    void onDownloadImageFinished(const QByteArray& reply, const QString& localPath);
+};
\ No newline at end of file
diff --git a/src/app/mainapplication.cpp b/src/app/mainapplication.cpp
index 6d2f4e4f1..7a858ca1a 100644
--- a/src/app/mainapplication.cpp
+++ b/src/app/mainapplication.cpp
@@ -25,7 +25,6 @@
 #include "appsettingsmanager.h"
 #include "connectivitymonitor.h"
 #include "systemtray.h"
-#include "previewengine.h"
 #include "videoprovider.h"
 
 #include <QAction>
@@ -133,7 +132,6 @@ MainApplication::init()
     connectivityMonitor_.reset(new ConnectivityMonitor(this));
     settingsManager_.reset(new AppSettingsManager(this));
     systemTray_.reset(new SystemTray(settingsManager_.get(), this));
-    previewEngine_.reset(new PreviewEngine(connectivityMonitor_.get(), this));
 
     QObject::connect(settingsManager_.get(),
                      &AppSettingsManager::retranslate,
@@ -347,7 +345,7 @@ MainApplication::initQmlLayer()
                          systemTray_.get(),
                          lrcInstance_.get(),
                          settingsManager_.get(),
-                         previewEngine_.get(),
+                         connectivityMonitor_.get(),
                          &screenInfo_,
                          this);
 
diff --git a/src/app/mainapplication.h b/src/app/mainapplication.h
index fa8c12b91..3e42b7744 100644
--- a/src/app/mainapplication.h
+++ b/src/app/mainapplication.h
@@ -20,6 +20,7 @@
 
 #pragma once
 
+#include "imagedownloader.h"
 #include "lrcinstance.h"
 #include "qtutils.h"
 
@@ -36,7 +37,6 @@ class ConnectivityMonitor;
 class AppSettingsManager;
 class SystemTray;
 class CallAdapter;
-class PreviewEngine;
 
 // Provides information about the screen the app is displayed on
 class ScreenInfo : public QObject
@@ -118,7 +118,7 @@ private:
     QScopedPointer<ConnectivityMonitor> connectivityMonitor_;
     QScopedPointer<AppSettingsManager> settingsManager_;
     QScopedPointer<SystemTray> systemTray_;
-    QScopedPointer<PreviewEngine> previewEngine_;
+    QScopedPointer<ImageDownloader> imageDownloader_;
 
     ScreenInfo screenInfo_;
 };
diff --git a/src/app/mainview/components/CachedImage.qml b/src/app/mainview/components/CachedImage.qml
new file mode 100644
index 000000000..d06063964
--- /dev/null
+++ b/src/app/mainview/components/CachedImage.qml
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 Savoir-faire Linux Inc.
+ *
+ * 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
+import QtQuick.Controls
+import QtQuick.Layouts
+import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+import net.jami.Helpers 1.1
+import net.jami.Models 1.1
+import "../../commoncomponents"
+
+Item {
+    id: cachedImage
+    property bool customLogo: false
+    property alias source: image.source
+    property string defaultImage: ""
+    property string downloadUrl: ""
+    property string fileExtension: downloadUrl.substring(downloadUrl.lastIndexOf("."), downloadUrl.length)
+    property string localPath: ""
+    property int imageFillMode: 0
+
+    Image {
+        id: image
+        objectName: "image"
+        anchors.fill: parent
+        fillMode: imageFillMode
+        smooth: true
+        antialiasing: true
+        property bool isSvg: getIsSvg(this)
+
+        Image {
+            id: default_img
+            objectName: "default_img"
+            anchors.fill: parent
+            source: defaultImage
+            visible: image.status != Image.Ready
+            smooth: true
+            antialiasing: true
+            property bool isSvg: getIsSvg(this)
+
+            Component.onCompleted: setSourceSize(default_img)
+        }
+
+        Component.onCompleted: setSourceSize(image)
+    }
+
+    function setSourceSize(img) {
+        img.sourceSize = undefined;
+        if (img.isSvg) {
+            img.sourceSize = Qt.size(cachedImage.width, cachedImage.height);
+        }
+    }
+
+    function getIsSvg(img) {
+        if (img.source && img.source!=""){
+            var localPath = img.source.toString()
+            if (localPath.startsWith("file://")) {
+                localPath = localPath.substring(7);
+            }
+            return UtilsAdapter.getMimeName(localPath).startsWith("image/svg+xml");
+        }
+        return false
+
+    }
+
+    Connections {
+        target: ImageDownloader
+        function onDownloadImageSuccessful(localPath) {
+            if (localPath === cachedImage.localPath) {
+                image.source = "file://" + localPath;
+            }
+        }
+        function onDownloadImageFailed(localPath) {
+            print("Failed to download image: " + downloadUrl);
+            if (localPath === cachedImage.localPath) {
+                image.source = defaultImage;
+            }
+        }
+    }
+
+    Connections {
+        target: cachedImage
+        function onDownloadUrlChanged() {
+            updateImageSource(downloadUrl, localPath, defaultImage);
+            setSourceSize(image);
+            setSourceSize(default_img);
+        }
+    }
+
+    Component.onCompleted: {
+        updateImageSource(downloadUrl, localPath, defaultImage);
+        setSourceSize(image);
+        setSourceSize(default_img);
+    }
+
+    Connections {
+        target: CurrentScreenInfo
+
+        function onDevicePixelRatioChanged() {
+            setSourceSize(image);
+            setSourceSize(default_img);
+        }
+    }
+
+    function updateImageSource(downloadUrl, localPath, defaultImage) {
+        if (downloadUrl === "") {
+            image.source = defaultImage;
+            return;
+        }
+        if (downloadUrl !== "" && localPath !== "") {
+            if (!UtilsAdapter.fileExists(localPath)) {
+                ImageDownloader.downloadImage(downloadUrl, localPath);
+            } else {
+                image.source = "file://" + localPath;
+            }
+        }
+    }
+}
diff --git a/src/app/qmlregister.cpp b/src/app/qmlregister.cpp
index 8a9f68fb5..15bc67a9c 100644
--- a/src/app/qmlregister.cpp
+++ b/src/app/qmlregister.cpp
@@ -26,7 +26,9 @@
 #include "messagesadapter.h"
 #include "positionmanager.h"
 #include "tipsmodel.h"
+#include "connectivitymonitor.h"
 #include "previewengine.h"
+#include "imagedownloader.h"
 #include "utilsadapter.h"
 #include "conversationsadapter.h"
 #include "currentcall.h"
@@ -54,7 +56,6 @@
 #include "mainapplication.h"
 #include "namedirectory.h"
 #include "updatemanager.h"
-#include "pluginlistmodel.h"
 #include "pluginlistpreferencemodel.h"
 #include "preferenceitemlistmodel.h"
 #include "wizardviewstepmodel.h"
@@ -105,12 +106,14 @@ registerTypes(QQmlEngine* engine,
               SystemTray* systemTray,
               LRCInstance* lrcInstance,
               AppSettingsManager* settingsManager,
-              PreviewEngine* previewEngine,
+              ConnectivityMonitor* connectivityMonitor,
               ScreenInfo* screenInfo,
               QObject* parent)
 {
     // setup the adapters (their lifetimes are that of MainApplication)
     auto callAdapter = new CallAdapter(systemTray, lrcInstance, parent);
+    auto previewEngine = new PreviewEngine(connectivityMonitor, parent);
+    auto imageDownloader = new ImageDownloader(connectivityMonitor, parent);
     auto messagesAdapter = new MessagesAdapter(settingsManager, previewEngine, lrcInstance, parent);
     auto positionManager = new PositionManager(settingsManager, systemTray, lrcInstance, parent);
     auto conversationsAdapter = new ConversationsAdapter(systemTray, lrcInstance, parent);
@@ -147,6 +150,7 @@ registerTypes(QQmlEngine* engine,
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentAccountToMigrate, "CurrentAccountToMigrate")
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_HELPERS, avatarRegistry, "AvatarRegistry");
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, wizardViewStepModel, "WizardViewStepModel")
+    QML_REGISTERSINGLETONTYPE_POBJECT(NS_HELPERS, imageDownloader, "ImageDownloader")
 
     // TODO: remove these
     QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, AVModel, &lrcInstance->avModel())
@@ -168,7 +172,6 @@ registerTypes(QQmlEngine* engine,
     QML_REGISTERTYPE(NS_MODELS, MediaCodecListModel);
     QML_REGISTERTYPE(NS_MODELS, AudioDeviceModel);
     QML_REGISTERTYPE(NS_MODELS, AudioManagerListModel);
-    QML_REGISTERTYPE(NS_MODELS, PluginListModel);
     QML_REGISTERTYPE(NS_MODELS, PreferenceItemListModel);
     QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
     QML_REGISTERTYPE(NS_MODELS, FilesToSendListModel);
diff --git a/src/app/qmlregister.h b/src/app/qmlregister.h
index 4676fd32d..a50cfee79 100644
--- a/src/app/qmlregister.h
+++ b/src/app/qmlregister.h
@@ -36,6 +36,7 @@ class AppSettingsManager;
 class PreviewEngine;
 class ScreenInfo;
 class MainApplication;
+class ConnectivityMonitor;
 
 // Hack for QtCreator autocomplete (part 1)
 // https://bugreports.qt.io/browse/QTCREATORBUG-20569
@@ -65,7 +66,7 @@ void registerTypes(QQmlEngine* engine,
                    SystemTray* systemTray,
                    LRCInstance* lrcInstance,
                    AppSettingsManager* appSettingsManager,
-                   PreviewEngine* previewEngine,
+                   ConnectivityMonitor* connectivityMonitor,
                    ScreenInfo* screenInfo,
                    QObject* parent);
 }
diff --git a/src/app/utilsadapter.cpp b/src/app/utilsadapter.cpp
index 9c5bc6296..aee1138a5 100644
--- a/src/app/utilsadapter.cpp
+++ b/src/app/utilsadapter.cpp
@@ -35,6 +35,8 @@
 #include <QClipboard>
 #include <QFileInfo>
 #include <QRegExp>
+#include <QMimeData>
+#include <QMimeDatabase>
 
 UtilsAdapter::UtilsAdapter(AppSettingsManager* settingsManager,
                            SystemTray* systemTray,
@@ -142,13 +144,21 @@ UtilsAdapter::getStyleSheet(const QString& name, const QString& source)
 }
 
 const QString
-UtilsAdapter::getCachePath()
+UtilsAdapter::getLocalDataPath()
 {
     QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
     dataDir.cdUp();
     return dataDir.absolutePath() + "/jami";
 }
 
+const QString
+UtilsAdapter::getCachePath()
+{
+    QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
+    dataDir.cdUp();
+    return dataDir.absolutePath() + "/jami";
+}
+
 QString
 UtilsAdapter::getDefaultRecordPath() const
 {
@@ -825,3 +835,58 @@ UtilsAdapter::isSystemTrayIconVisible()
         return false;
     return systemTray_->geometry() != QRect();
 }
+
+QString
+UtilsAdapter::base64Encode(const QString& input)
+{
+    QByteArray byteArray = input.toUtf8();
+    return byteArray.toBase64();
+}
+
+bool
+UtilsAdapter::fileExists(const QString& filePath)
+{
+    return QFile::exists(filePath);
+}
+
+QString
+UtilsAdapter::getStandardTempLocation()
+{
+    return QStandardPaths::writableLocation(
+        static_cast<QStandardPaths::StandardLocation>(QStandardPaths::TempLocation));
+}
+
+QString
+UtilsAdapter::getMimeName(const QString &filePath) const
+{
+    QMimeDatabase db;
+    QMimeType mime = db.mimeTypeForFile(filePath);
+    return mime.name();
+}
+
+#ifdef ENABLE_TESTS
+//Must only be used for testing purposes
+QString
+UtilsAdapter::createDummyImage() const
+{
+    // Create an QImage
+    QImage image(256, 256, QImage::Format_ARGB32);
+    image.fill(QColor(255, 255, 255, 255));
+
+    QByteArray ba;
+    QBuffer bu(&ba);
+    image.save(&bu, "PNG");
+
+    // Save the image to a file
+    QFile file(QDir::tempPath() + "/dummy.png");
+    if (file.open(QIODevice::WriteOnly)) {
+        file.write(ba);
+        file.close();
+        qInfo() << "Dummy image created" << QDir::tempPath() + "/dummy.png";
+        return QDir::tempPath() + "/dummy.png";
+    } else {
+        qWarning() << "Could not create dummy image";
+        return "";
+    }
+}
+#endif
diff --git a/src/app/utilsadapter.h b/src/app/utilsadapter.h
index 3325e1f29..8aa5eea98 100644
--- a/src/app/utilsadapter.h
+++ b/src/app/utilsadapter.h
@@ -86,6 +86,7 @@ public:
     Q_INVOKABLE void setClipboardText(QString text);
     Q_INVOKABLE const QString qStringFromFile(const QString& filename);
     Q_INVOKABLE const QString getStyleSheet(const QString& name, const QString& source);
+    Q_INVOKABLE const QString getLocalDataPath();
     Q_INVOKABLE const QString getCachePath();
     Q_INVOKABLE bool createStartupLink();
     Q_INVOKABLE QString GetRingtonePath();
@@ -152,6 +153,14 @@ public:
     Q_INVOKABLE bool isRTL();
     Q_INVOKABLE bool isSystemTrayIconVisible();
 
+    Q_INVOKABLE QString base64Encode(const QString& input);
+    Q_INVOKABLE bool fileExists(const QString& filePath);
+    Q_INVOKABLE QString getStandardTempLocation();
+    Q_INVOKABLE QString getMimeName(const QString& filePath) const;
+
+#ifdef ENABLE_TESTS
+    Q_INVOKABLE QString createDummyImage() const;
+#endif
 Q_SIGNALS:
     void debugMessageReceived(const QString& message);
     void changeFontSize();
diff --git a/src/app/webengine/GeneralWebEngineView.qml b/src/app/webengine/GeneralWebEngineView.qml
index da8e2dfef..d89868128 100644
--- a/src/app/webengine/GeneralWebEngineView.qml
+++ b/src/app/webengine/GeneralWebEngineView.qml
@@ -62,8 +62,8 @@ WebEngineView {
     }
 
     Component.onCompleted: {
-        profile.cachePath = UtilsAdapter.getCachePath();
-        profile.persistentStoragePath = UtilsAdapter.getCachePath();
+        profile.cachePath = UtilsAdapter.getLocalDataPath();
+        profile.persistentStoragePath = UtilsAdapter.getLocalDataPath();
         profile.persistentCookiesPolicy = WebEngineProfile.NoPersistentCookies;
         profile.httpCacheType = WebEngineProfile.NoCache;
         profile.httpUserAgent = JamiStrings.httpUserAgentName;
diff --git a/tests/qml/main.cpp b/tests/qml/main.cpp
index 0e92ed184..23edaa66a 100644
--- a/tests/qml/main.cpp
+++ b/tests/qml/main.cpp
@@ -90,7 +90,7 @@ public Q_SLOTS:
                              systemTray_.get(),
                              lrcInstance_.get(),
                              settingsManager_.get(),
-                             previewEngine_.get(),
+                             connectivityMonitor_.get(),
                              &screenInfo_,
                              this);
 
diff --git a/tests/qml/src/tst_CachedImage.qml b/tests/qml/src/tst_CachedImage.qml
new file mode 100644
index 000000000..af04a7dac
--- /dev/null
+++ b/tests/qml/src/tst_CachedImage.qml
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 Savoir-faire Linux Inc.
+ *
+ * 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
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import QtTest
+
+import net.jami.Adapters 1.1
+import net.jami.Models 1.1
+import net.jami.Constants 1.1
+import net.jami.Enums 1.1
+import net.jami.Helpers 1.1
+
+import "../../../src/app/"
+import "../../../src/app/mainview/components"
+
+Item {
+
+    width: 800
+    height: 600
+
+    CachedImage {
+        id: cachedImage
+
+        TestCase {
+            name: "Test cachedImage"
+            when: windowShown
+
+            SignalSpy {
+                id: spyDownloadSuccessful
+                target: ImageDownloader
+                signalName: "onDownloadImageSuccessful"
+            }
+
+            SignalSpy {
+                id: spyDownloadFailed
+                target: ImageDownloader
+                signalName: "onDownloadImageFailed"
+            }
+
+            function test_goodDownLoad() {
+
+                var localPath = UtilsAdapter.getStandardTempLocation()+"/"+Math.random().toString(36).substring(7)+".svg"
+
+                cachedImage.localPath = localPath
+                cachedImage.downloadUrl= "File://"+UtilsAdapter.createDummyImage()
+
+                spyDownloadSuccessful.wait()
+
+                compare(findChild(cachedImage,"image").source, Qt.url("file://"+localPath), "image source")
+                compare(findChild(cachedImage,"default_img").visible,false, "default_img visible")
+
+            }
+
+            function test_failedDownLoad() {
+                var imageUrl = "File:///dummy"
+
+                var localPath = UtilsAdapter.getStandardTempLocation()+"/"+Math.random().toString(36).substring(7)+".svg"
+
+                cachedImage.localPath = localPath
+                cachedImage.downloadUrl= imageUrl
+
+                spyDownloadFailed.wait()
+
+                compare(findChild(cachedImage,"image").source,"", "image source")
+                compare(findChild(cachedImage,"image").visible,true, "default_img visible")
+            }
+        }
+    }
+}
-- 
GitLab