From 623333ab8cdce11937311ff0b5d1b905623acc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tinkl?= Date: Fri, 1 Nov 2024 10:13:00 +0100 Subject: [PATCH] fix: improve image type detection - use the same approach as status-go to detect the image type, relying on "magic" type matching instead of looking at the file extension (now using C++ and QMime*) - add a little error popup when the user tries to upload an unsupported image type while creating/editing a community - expose all the image related properties from the C++ backend instead of constructing and duplicating them in QML - cleanup some unused/dead code Fixes #16668 --- ui/StatusQ/include/StatusQ/urlutils.h | 32 +++++++++++--- ui/StatusQ/src/typesregistration.cpp | 4 +- ui/StatusQ/src/urlutils.cpp | 43 +++++++++++++------ .../Communities/stores/CommunitiesStore.qml | 21 --------- .../Profile/views/CommunitiesView.qml | 6 ++- .../shared/popups/ImageCropWorkflow.qml | 30 ++++++++++++- .../StatusChatImageExtensionValidator.qml | 4 +- ui/imports/shared/status/StatusChatInput.qml | 2 +- ui/imports/utils/Constants.qml | 1 - ui/imports/utils/Utils.qml | 3 +- 10 files changed, 95 insertions(+), 51 deletions(-) diff --git a/ui/StatusQ/include/StatusQ/urlutils.h b/ui/StatusQ/include/StatusQ/urlutils.h index ae71d0226a..022f478885 100644 --- a/ui/StatusQ/include/StatusQ/urlutils.h +++ b/ui/StatusQ/include/StatusQ/urlutils.h @@ -1,18 +1,38 @@ #pragma once #include - -class QQmlEngine; -class QJSEngine; +#include class UrlUtils : public QObject { Q_OBJECT + Q_PROPERTY(QString validImageNameFilters READ validImageNameFilters FINAL CONSTANT) // "*.jpg *.jpe *.jp *.jpeg *.png *.webp *.gif *.svg" + Q_PROPERTY(QStringList validPreferredImageExtensions READ validPreferredImageExtensions FINAL CONSTANT) // ["jpg", "png", "webp", "gif", "svg"] + Q_PROPERTY(QStringList allValidImageExtensions READ allValidImageExtensions FINAL CONSTANT) // ["jpg", "jpe", "jp", "jpeg", "png", "webp", "gif", "svg"] public: - static QObject* qmlInstance(QQmlEngine* engine, QJSEngine* scriptEngine); + explicit UrlUtils(QObject* parent = nullptr); - Q_INVOKABLE static bool isValidImageUrl(const QUrl &url, - const QStringList &acceptedExtensions); + Q_INVOKABLE bool isValidImageUrl(const QUrl &url) const; Q_INVOKABLE static qint64 getFileSize(const QUrl &url); + +private: + QMimeDatabase m_mimeDb; + + QStringList m_validImageMimeTypes{QStringLiteral("image/jpeg"), + QStringLiteral("image/png"), + QStringLiteral("image/gif"), + QStringLiteral("image/svg")}; + + // "*.jpg *.jpe *.jp *.jpeg *.png *.webp *.gif *.svg" + QString m_imgFilters; + QString validImageNameFilters() const { return m_imgFilters; } + + // ["jpg", "png", "webp", "gif", "svg"] + QStringList m_imgExtensions; + QStringList validPreferredImageExtensions() const { return m_imgExtensions; } + + // ["jpg", "jpe", "jp", "jpeg", "png", "webp", "gif", "svg"] + QStringList m_allImgExtensions; + QStringList allValidImageExtensions() const { return m_allImgExtensions; } }; diff --git a/ui/StatusQ/src/typesregistration.cpp b/ui/StatusQ/src/typesregistration.cpp index e64e15478b..d40d10abe1 100644 --- a/ui/StatusQ/src/typesregistration.cpp +++ b/ui/StatusQ/src/typesregistration.cpp @@ -71,7 +71,9 @@ void registerStatusQTypes() { qmlRegisterType("StatusQ", 0, 1, "FormattedDoubleProperty"); qmlRegisterSingletonType("StatusQ", 0, 1, "ClipboardUtils", &ClipboardUtils::qmlInstance); - qmlRegisterSingletonType("StatusQ", 0, 1, "UrlUtils", &UrlUtils::qmlInstance); + qmlRegisterSingletonType("StatusQ", 0, 1, "UrlUtils", [](QQmlEngine* engine, QJSEngine*) { + return new UrlUtils(engine); + }); qmlRegisterType("StatusQ", 0, 1, "ModelEntry"); qmlRegisterType("StatusQ", 0, 1, "SnapshotObject"); diff --git a/ui/StatusQ/src/urlutils.cpp b/ui/StatusQ/src/urlutils.cpp index 72f44df34e..caf9813500 100644 --- a/ui/StatusQ/src/urlutils.cpp +++ b/ui/StatusQ/src/urlutils.cpp @@ -1,25 +1,40 @@ #include "StatusQ/urlutils.h" #include +#include #include -#include - -QObject* UrlUtils::qmlInstance(QQmlEngine*, QJSEngine*) -{ - return new UrlUtils; +namespace { +constexpr auto webpMime = "image/webp"; } -bool UrlUtils::isValidImageUrl(const QUrl& url, const QStringList& acceptedExtensions) -{ - const auto strippedUrl = url.url( - QUrl::RemoveAuthority | QUrl::RemoveFragment | QUrl::RemoveQuery); +UrlUtils::UrlUtils(QObject *parent): QObject(parent) { + const auto webpSupported = QImageReader::supportedMimeTypes().contains(webpMime); + if (webpSupported) + m_validImageMimeTypes.append(webpMime); - return std::any_of(acceptedExtensions.constBegin(), - acceptedExtensions.constEnd(), - [strippedUrl](const auto & ext) { - return strippedUrl.endsWith(ext, Qt::CaseInsensitive); - }); + QStringList imgFilters; + for (const auto& mime: std::as_const(m_validImageMimeTypes)) { + const auto mimeData = m_mimeDb.mimeTypeForName(mime); + imgFilters.append(mimeData.globPatterns()); + m_imgExtensions.append(mimeData.preferredSuffix()); + m_allImgExtensions.append(mimeData.suffixes()); + } + + m_imgFilters = imgFilters.join(' '); + m_imgFilters.append(QStringLiteral(" ")); + m_imgFilters.append(m_imgFilters.toUpper()); // include the uppercase extensions too for case sensitive file systems +} + +bool UrlUtils::isValidImageUrl(const QUrl &url) const +{ + QString mimeType; + if (url.isLocalFile()) + mimeType = m_mimeDb.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchContent).name(); + else + mimeType = m_mimeDb.mimeTypeForUrl(url).name(); + + return m_validImageMimeTypes.contains(mimeType); } qint64 UrlUtils::getFileSize(const QUrl& url) diff --git a/ui/app/AppLayouts/Communities/stores/CommunitiesStore.qml b/ui/app/AppLayouts/Communities/stores/CommunitiesStore.qml index 6d52c6258d..3c9805555c 100644 --- a/ui/app/AppLayouts/Communities/stores/CommunitiesStore.qml +++ b/ui/app/AppLayouts/Communities/stores/CommunitiesStore.qml @@ -38,27 +38,6 @@ QtObject { readonly property bool createCommunityEnabled: localAppSettings.createCommunityEnabled ?? false readonly property bool testEnvironment: localAppSettings.testEnvironment ?? false - // TODO: Could the backend provide directly 2 filtered models?? - //property var featuredCommunitiesModel: root.communitiesModuleInst.curatedFeaturedCommunities - //property var popularCommunitiesModel: root.communitiesModuleInst.curatedPopularCommunities - property ListModel tagsModel: ListModel {}//root.notionsTagsModel - - // TO DO: Complete list can be added in backend or here: https://www.notion.so/Category-tags-339b2e699e7c4d36ab0608ab00b99111 - property ListModel notionsTagsModel : ListModel { - ListElement { name: "gaming"; emoji: "🎮"} - ListElement { name: "art"; emoji: "🖼️️"} - ListElement { name: "crypto"; emoji: "💸"} - ListElement { name: "nsfw"; emoji: "🍆"} - ListElement { name: "markets"; emoji: "💎"} - ListElement { name: "defi"; emoji: "📈"} - ListElement { name: "travel"; emoji: "🚁"} - ListElement { name: "web3"; emoji: "🗺"} - ListElement { name: "sport"; emoji: "🎾"} - ListElement { name: "food"; emoji: "🥑"} - ListElement { name: "enviroment"; emoji: "☠️"} - ListElement { name: "privacy"; emoji: "👻"} - } - property string communityTags: communitiesModuleInst.tags signal importingCommunityStateChanged(string communityId, int state, string errorMsg) diff --git a/ui/app/AppLayouts/Profile/views/CommunitiesView.qml b/ui/app/AppLayouts/Profile/views/CommunitiesView.qml index 14f25c8703..ed60c94e71 100644 --- a/ui/app/AppLayouts/Profile/views/CommunitiesView.qml +++ b/ui/app/AppLayouts/Profile/views/CommunitiesView.qml @@ -147,11 +147,12 @@ SettingsContentBase { Panel { id: panelMembers - filters: ExpressionFilter { + filters: FastExpressionFilter { readonly property int ownerRole: Constants.memberRole.owner readonly property int adminRole: Constants.memberRole.admin readonly property int tokenMasterRole: Constants.memberRole.tokenMaster expression: model.joined && model.memberRole !== ownerRole && model.memberRole !== adminRole && model.memberRole !== tokenMasterRole + expectedRoles: ["joined", "memberRole"] } } @@ -162,8 +163,9 @@ SettingsContentBase { Panel { id: panelPendingRequests - filters: ExpressionFilter { + filters: FastExpressionFilter { expression: model.spectated && !model.joined + expectedRoles: ["joined", "spectated"] } } } diff --git a/ui/imports/shared/popups/ImageCropWorkflow.qml b/ui/imports/shared/popups/ImageCropWorkflow.qml index 9e3e216fe4..65c20efecd 100644 --- a/ui/imports/shared/popups/ImageCropWorkflow.qml +++ b/ui/imports/shared/popups/ImageCropWorkflow.qml @@ -1,12 +1,15 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 import QtQuick.Dialogs 1.3 +import StatusQ 0.1 import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Popups 0.1 +import StatusQ.Popups.Dialog 0.1 import utils 1.0 @@ -39,14 +42,37 @@ Item { title: root.imageFileDialogTitle folder: root.userSelectedImage ? imageCropper.source.substr(0, imageCropper.source.lastIndexOf("/")) : shortcuts.pictures - nameFilters: [qsTr("Supported image formats (%1)").arg(Constants.acceptedDragNDropImageExtensions.map(img => "*" + img).join(" "))] + nameFilters: [qsTr("Supported image formats (%1)").arg(UrlUtils.validImageNameFilters)] onAccepted: { if (fileDialog.fileUrls.length > 0) { - cropImage(fileDialog.fileUrls[0]) + const url = fileDialog.fileUrls[0] + if (Utils.isValidDragNDropImage(url)) + cropImage(url) + else + errorDialog.open() } } } // FileDialog + StatusDialog { + id: errorDialog + title: qsTr("Image format not supported") + width: 480 + contentItem: ColumnLayout { + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: qsTr("Format of the image you chose is not supported. Most probably you picked a file that is invalid, corrupted or has a wrong file extension.") + } + StatusBaseText { + Layout.fillWidth: true + font.pixelSize: Theme.additionalTextSize + text: qsTr("Supported image extensions: %1").arg(UrlUtils.allValidImageExtensions) + } + } + standardButtons: Dialog.Ok + } // StatusDialog + StatusModal { id: imageCropperModal diff --git a/ui/imports/shared/status/StatusChatImageExtensionValidator.qml b/ui/imports/shared/status/StatusChatImageExtensionValidator.qml index a76d36c4cd..ab78e8d433 100644 --- a/ui/imports/shared/status/StatusChatImageExtensionValidator.qml +++ b/ui/imports/shared/status/StatusChatImageExtensionValidator.qml @@ -1,12 +1,14 @@ import QtQuick 2.15 +import StatusQ 0.1 + import utils 1.0 StatusChatImageValidator { id: root errorMessage: qsTr("Format not supported.") - secondaryErrorMessage: qsTr("Upload %1 only").arg(Constants.acceptedDragNDropImageExtensions.map(ext => ext.replace(".", "").toUpperCase() + "s").join(", ")) + secondaryErrorMessage: qsTr("Upload %1 only").arg(UrlUtils.validPreferredImageExtensions.map(ext => ext.toUpperCase() + "s").join(", ")) onImagesChanged: { let isValid = true diff --git a/ui/imports/shared/status/StatusChatInput.qml b/ui/imports/shared/status/StatusChatInput.qml index 78af44d141..735fbedb23 100644 --- a/ui/imports/shared/status/StatusChatInput.qml +++ b/ui/imports/shared/status/StatusChatInput.qml @@ -982,7 +982,7 @@ Rectangle { folder: shortcuts.pictures selectMultiple: true nameFilters: [ - qsTr("Image files (%1)").arg(Constants.acceptedDragNDropImageExtensions.map(img => "*" + img).join(" ")) + qsTr("Image files (%1)").arg(UrlUtils.validImageNameFilters) ] onAccepted: { imageBtn.highlighted = false diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 42e360da59..bb6aced82c 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -955,7 +955,6 @@ QtObject { readonly property int maxNumberOfPins: 3 readonly property string dataImagePrefix: "data:image" - readonly property var acceptedDragNDropImageExtensions: [".png", ".jpg", ".jpeg"] readonly property string mentionSpanTag: `` diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index 7b281b438c..9609442a8c 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -276,8 +276,7 @@ QtObject { } function isValidDragNDropImage(url) { - return url.startsWith(Constants.dataImagePrefix) || - UrlUtils.isValidImageUrl(url, Constants.acceptedDragNDropImageExtensions) + return url.startsWith(Constants.dataImagePrefix) || UrlUtils.isValidImageUrl(url) } function isFilesizeValid(img) {