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
This commit is contained in:
Lukáš Tinkl 2024-11-01 10:13:00 +01:00 committed by Lukáš Tinkl
parent e3238b3fd2
commit 623333ab8c
10 changed files with 95 additions and 51 deletions

View File

@ -1,18 +1,38 @@
#pragma once
#include <QObject>
class QQmlEngine;
class QJSEngine;
#include <QMimeDatabase>
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; }
};

View File

@ -71,7 +71,9 @@ void registerStatusQTypes() {
qmlRegisterType<FormattedDoubleProperty>("StatusQ", 0, 1, "FormattedDoubleProperty");
qmlRegisterSingletonType<ClipboardUtils>("StatusQ", 0, 1, "ClipboardUtils", &ClipboardUtils::qmlInstance);
qmlRegisterSingletonType<UrlUtils>("StatusQ", 0, 1, "UrlUtils", &UrlUtils::qmlInstance);
qmlRegisterSingletonType<UrlUtils>("StatusQ", 0, 1, "UrlUtils", [](QQmlEngine* engine, QJSEngine*) {
return new UrlUtils(engine);
});
qmlRegisterType<ModelEntry>("StatusQ", 0, 1, "ModelEntry");
qmlRegisterType<SnapshotObject>("StatusQ", 0, 1, "SnapshotObject");

View File

@ -1,25 +1,40 @@
#include "StatusQ/urlutils.h"
#include <QFile>
#include <QImageReader>
#include <QUrl>
#include <algorithm>
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)

View File

@ -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)

View File

@ -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"]
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: `<span style="background-color: ${Theme.palette.mentionColor2};"><a style="color:${Theme.palette.mentionColor1};text-decoration:none" href='http://'>`

View File

@ -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) {