feat(@desktop/wallet): Added feature flag FLAG_SEND_VIA_PERSONAL_CHAT_ENABLED for the send via personal chat feature

Also added logic in order to detect and highlight an address/ens name in the chat
This commit is contained in:
Khushboo Mehta 2024-09-17 13:34:24 +02:00 committed by Khushboo-dev-cpp
parent baf3061fda
commit 5771a33eaa
15 changed files with 171 additions and 2 deletions

View File

@ -4,6 +4,7 @@ import os
const DEFAULT_FLAG_DAPPS_ENABLED = false const DEFAULT_FLAG_DAPPS_ENABLED = false
const DEFAULT_FLAG_SWAP_ENABLED = true const DEFAULT_FLAG_SWAP_ENABLED = true
const DEFAULT_FLAG_CONNECTOR_ENABLED* = false const DEFAULT_FLAG_CONNECTOR_ENABLED* = false
const DEFAULT_FLAG_SEND_VIA_PERSONAL_CHAT_ENABLED = false
proc boolToEnv*(defaultValue: bool): string = proc boolToEnv*(defaultValue: bool): string =
return if defaultValue: "1" else: "0" return if defaultValue: "1" else: "0"
@ -13,12 +14,14 @@ QtObject:
dappsEnabled: bool dappsEnabled: bool
swapEnabled: bool swapEnabled: bool
connectorEnabled: bool connectorEnabled: bool
sendViaPersonalChatEnabled: bool
proc setup(self: FeatureFlags) = proc setup(self: FeatureFlags) =
self.QObject.setup() self.QObject.setup()
self.dappsEnabled = getEnv("FLAG_DAPPS_ENABLED", boolToEnv(DEFAULT_FLAG_DAPPS_ENABLED)) != "0" self.dappsEnabled = getEnv("FLAG_DAPPS_ENABLED", boolToEnv(DEFAULT_FLAG_DAPPS_ENABLED)) != "0"
self.swapEnabled = getEnv("FLAG_SWAP_ENABLED", boolToEnv(DEFAULT_FLAG_SWAP_ENABLED)) != "0" self.swapEnabled = getEnv("FLAG_SWAP_ENABLED", boolToEnv(DEFAULT_FLAG_SWAP_ENABLED)) != "0"
self.connectorEnabled = getEnv("FLAG_CONNECTOR_ENABLED", boolToEnv(DEFAULT_FLAG_CONNECTOR_ENABLED)) != "0" self.connectorEnabled = getEnv("FLAG_CONNECTOR_ENABLED", boolToEnv(DEFAULT_FLAG_CONNECTOR_ENABLED)) != "0"
self.sendViaPersonalChatEnabled = getEnv("FLAG_SEND_VIA_PERSONAL_CHAT_ENABLED", boolToEnv(DEFAULT_FLAG_SEND_VIA_PERSONAL_CHAT_ENABLED)) != "0"
proc delete*(self: FeatureFlags) = proc delete*(self: FeatureFlags) =
self.QObject.delete() self.QObject.delete()
@ -44,3 +47,9 @@ QtObject:
QtProperty[bool] connectorEnabled: QtProperty[bool] connectorEnabled:
read = getConnectorEnabled read = getConnectorEnabled
proc getSendViaPersonalChatEnabled*(self: FeatureFlags): bool {.slot.} =
return self.sendViaPersonalChatEnabled
QtProperty[bool] sendViaPersonalChatEnabled:
read = getSendViaPersonalChatEnabled

View File

@ -128,6 +128,30 @@ SplitView {
outgoingStatus: StatusMessage.OutgoingStatus.Expired outgoingStatus: StatusMessage.OutgoingStatus.Expired
resendError: "can't send message on Tuesday" resendError: "can't send message on Tuesday"
} }
ListElement {
timestamp: 1667937930159
senderId: "zqdeadbeef"
senderDisplayName: "replicator.stateofus.eth"
contentType: StatusMessage.ContentType.Text
message: "Test message with a link https://github.com/. Hey annyah! 0x16437e05858c1a34f0ae63c9ca960d61a5583d5e
this is my wallet address eth:opt:arb:0x16437e05858c1a34f0ae63c9ca960d61a5583d5e,
0x75d5673fc25bb4993ea1218d9d415487c3656853"
isContact: true
isAReply: true
trustIndicator: StatusContactVerificationIcons.TrustedType.None
outgoingStatus: StatusMessage.OutgoingStatus.Delivered
}
ListElement {
timestamp: 1667937930159
senderId: "zqdeadbeef"
senderDisplayName: "replicator.stateofus.eth"
contentType: StatusMessage.ContentType.Text
message: "Ola!! qwerty.stateofus.eth hey this is my ens name"
isContact: true
isAReply: false
trustIndicator: StatusContactVerificationIcons.TrustedType.None
outgoingStatus: StatusMessage.OutgoingStatus.Delivered
}
} }
readonly property var colorHash: ListModel { readonly property var colorHash: ListModel {
ListElement { colorId: 13; segmentLength: 5 } ListElement { colorId: 13; segmentLength: 5 }
@ -161,6 +185,7 @@ SplitView {
isAReply: model.isAReply isAReply: model.isAReply
outgoingStatus: model.outgoingStatus outgoingStatus: model.outgoingStatus
resendError: model.outgoingStatus === StatusMessage.OutgoingStatus.Expired ? model.resendError : "" resendError: model.outgoingStatus === StatusMessage.OutgoingStatus.Expired ? model.resendError : ""
linkAddressAndEnsName: true
messageDetails { messageDetails {
readonly property bool isEnsVerified: model.senderDisplayName.endsWith(".eth") readonly property bool isEnsVerified: model.senderDisplayName.endsWith(".eth")
@ -196,6 +221,7 @@ SplitView {
onReplyProfileClicked: logs.logEvent("StatusMessage::replyProfileClicked") onReplyProfileClicked: logs.logEvent("StatusMessage::replyProfileClicked")
onReplyMessageClicked: logs.logEvent("StatusMessage::replyMessageClicked") onReplyMessageClicked: logs.logEvent("StatusMessage::replyMessageClicked")
onResendClicked: logs.logEvent("StatusMessage::resendClicked") onResendClicked: logs.logEvent("StatusMessage::resendClicked")
onLinkActivated: logs.logEvent("StatusMessage::linkActivated" + link)
} }
} }
} }

View File

@ -0,0 +1,90 @@
import QtQuick 2.15
import QtTest 1.15
import StatusQ.Components 0.1
Item {
id: root
width: 600
height: 400
Component {
id: componentUnderTest
StatusMessage {
anchors.fill: parent
messageDetails {
messageText: ""
contentType: StatusMessage.ContentType.Text
amISender: false
sender.id: "zq123456790"
sender.displayName: "Alice"
sender.isContact: true
sender.trustIndicator: StatusContactVerificationIcons.TrustedType.None
sender.isEnsVerified: false
sender.profileImage {
name: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAiElEQVR4nOzXUQpAQBRGYWQvLNAyLJDV8C5qpiGnv/M9al5Ot27X0IUwhMYQGkNoDKGJCRlLH67bftx9X+ap/+P9VcxEDKExhKZ4a9Uq3TZviZmIITSG0DRvlqcbqVbrlouZiCE0htD4h0hjCI0hNN5aNIbQGKKPxEzEEBpDaAyhMYTmDAAA//+gYCErzmCpCQAAAABJRU5ErkJggg="
colorId: 1
colorHash: "#51D0F0"
}
}
linkAddressAndEnsName: true
outgoingStatus: StatusMessage.OutgoingStatus.Sending
}
}
property StatusMessage controlUnderTest: null
TestCase {
name: "TokenSelectorView"
when: windowShown
function init() {
controlUnderTest = createTemporaryObject(componentUnderTest, root)
}
function test_different_address_formats_data() {
return [
{messageText: "0x1234567890abcdef1234567890abcdef12345678", validAddressEnsCount: 1}, // Valid ETH address
{messageText: "0x1234567890abcdef1234567890abcdef12345678,
0x16437e05858c1a34f0ae63c9ca960d61a5583d5e,
0x75d5673fc25bb4993ea1218d9d415487c3656853", validAddressEnsCount: 3}, // Valid ETH address
{messageText: "0xAbCdEf1234567890abcdef1234567890AbCdEf12", validAddressEnsCount: 1}, // Valid ETH address
{messageText: "0x123", validAddressEnsCount: 0}, // Invalid ETH address (too short)
{messageText: "1234567890abcdef1234567890abcdef12345678", validAddressEnsCount: 0}, // Invalid ETH address (no 0x)
{messageText: "qwerty.stateofus.eth", validAddressEnsCount: 1}, // Valid ETH address
{messageText: "alice.eth", validAddressEnsCount: 1}, // Valid ENS name
{messageText: "bob.eth", validAddressEnsCount: 1}, // Valid ENS name
{messageText: "sub.alice.eth", validAddressEnsCount: 1}, // Valid ENS name with subdomain
{messageText: "bob.stateofus.eth", validAddressEnsCount: 1}, // Valid ENS name with subdomain
{messageText: "ens.sub.sub.eth", validAddressEnsCount: 1}, // Valid ENS name with multiple subdomains
{messageText: "example.com", validAddressEnsCount: 0}, // Invalid DNS-based ENS name
{messageText: "another.example.xyz", validAddressEnsCount: 0}, // Invalid DNS-based ENS name
{messageText: "my-site.io", validAddressEnsCount: 0}, // Invalid DNS-based ENS name
{messageText: "invalid.ethaddress", validAddressEnsCount: 0}, // Invalid ENS-like name
{messageText: "bob.eth.invalid", validAddressEnsCount: 0}, // Invalid ENS-like name (invalid TLD)
{messageText: "My wallet is 0x1234567890abcdef1234567890abcdef12345678, and my ENS is alice.eth.", validAddressEnsCount: 2}, // Valid ETH and ENS in sentence
{messageText: "You can find me at bob.eth or contact me via 0xAbCdEf1234567890abcdef1234567890AbCdEf12.", validAddressEnsCount: 2}, // Valid ETH and ENS in sentence
{messageText: "Invalid address: 0x12345 and valid ENS: sub.alice.eth.", validAddressEnsCount: 1}, // Mixed case with valid and invalid
{messageText: "Check 0x123GHIJKLMNOPQRSTUVWXYZ and visit example.com.", validAddressEnsCount: 0}, // Mixed case with valid DNS and invalid ETH
{messageText: "0x1234567890abcdef1234567890abcdef12345678, qwerty.stateofus.eth, 0x16437e05858c1a34f0ae63c9ca960d61a5583d5e, 0x75d5673fc25bb4993ea1218d9d415487c3656853", validAddressEnsCount: 4}, // Valid ETH address
]
}
function test_different_address_formats(data) {
verify(!!controlUnderTest)
controlUnderTest.messageDetails.messageText = data.messageText
waitForRendering(controlUnderTest)
const statusTextMessage = findChild(controlUnderTest, "StatusMessage_textMessage")
verify(!!statusTextMessage)
// Use regular expression to match all <a> tags in the text
var linkMatches = statusTextMessage.textField.text.match(/<a\b[^>]*>(.*?)<\/a>/gi)
var actualLinkCount = linkMatches ? linkMatches.length : 0
compare(actualLinkCount, data.validAddressEnsCount, "TextEdit should contain a link %1".arg(data.messageText))
}
}
}

View File

@ -75,6 +75,7 @@ Control {
property bool isInPinnedPopup property bool isInPinnedPopup
property string highlightedLink: "" property string highlightedLink: ""
property string hoveredLink: "" property string hoveredLink: ""
property bool linkAddressAndEnsName
property StatusMessageDetails messageDetails: StatusMessageDetails {} property StatusMessageDetails messageDetails: StatusMessageDetails {}
property StatusMessageDetails replyDetails: StatusMessageDetails {} property StatusMessageDetails replyDetails: StatusMessageDetails {}
@ -288,6 +289,7 @@ Control {
allowShowMore: !root.isInPinnedPopup allowShowMore: !root.isInPinnedPopup
textField.anchors.rightMargin: root.isInPinnedPopup ? /*Style.current.xlPadding*/ 32 : 0 // margin for the "Unpin" floating button textField.anchors.rightMargin: root.isInPinnedPopup ? /*Style.current.xlPadding*/ 32 : 0 // margin for the "Unpin" floating button
highlightedLink: root.highlightedLink highlightedLink: root.highlightedLink
linkAddressAndEnsName: root.linkAddressAndEnsName
onLinkActivated: { onLinkActivated: {
root.linkActivated(link); root.linkActivated(link);
} }

View File

@ -13,6 +13,7 @@ Item {
readonly property alias hoveredLink: chatText.hoveredLink readonly property alias hoveredLink: chatText.hoveredLink
property string highlightedLink: "" property string highlightedLink: ""
property bool linkAddressAndEnsName
property StatusMessageDetails messageDetails: StatusMessageDetails {} property StatusMessageDetails messageDetails: StatusMessageDetails {}
property bool isEdited: false property bool isEdited: false
@ -42,7 +43,7 @@ Item {
if (root.messageDetails.contentType === StatusMessage.ContentType.Emoji && !root.isEdited) if (root.messageDetails.contentType === StatusMessage.ContentType.Emoji && !root.isEdited)
return Emoji.parse(root.messageDetails.messageText, Emoji.size.middle, Emoji.format.png); return Emoji.parse(root.messageDetails.messageText, Emoji.size.middle, Emoji.format.png);
let formattedMessage = Utils.linkifyAndXSS(root.messageDetails.messageText); let formattedMessage = Utils.linkifyAndXSS(root.messageDetails.messageText, root.linkAddressAndEnsName);
isQuote = (formattedMessage.startsWith("<blockquote>") && formattedMessage.endsWith("</blockquote>")); isQuote = (formattedMessage.startsWith("<blockquote>") && formattedMessage.endsWith("</blockquote>"));

View File

@ -157,7 +157,7 @@ QtObject {
} }
} }
function linkifyAndXSS(inputText) { function linkifyAndXSS(inputText, linkAddressAndEnsName = false) {
//URLs starting with http://, https://, ftp:// or status-app:// //URLs starting with http://, https://, ftp:// or status-app://
var replacePattern1 = /(\b(https?|ftp|status-app):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;\(\)]*[-A-Z0-9+&@#\/%=~_|])/gim; var replacePattern1 = /(\b(https?|ftp|status-app):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;\(\)]*[-A-Z0-9+&@#\/%=~_|])/gim;
var replacedText = inputText.replace(replacePattern1, "<a href='$1'>$1</a>"); var replacedText = inputText.replace(replacePattern1, "<a href='$1'>$1</a>");
@ -166,6 +166,18 @@ QtObject {
var replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim; var replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, "$1<a href='http://$2'>$2</a>"); replacedText = replacedText.replace(replacePattern2, "$1<a href='http://$2'>$2</a>");
if (linkAddressAndEnsName) {
// Wallet address
var replacePatternWalletAddress = /(^|[^\/])(0x[a-fA-F0-9]{40})/gim;
replacedText = replacedText.replace(replacePatternWalletAddress, "$1<a href='//send-via-personal-chat//$2'>$2</a>");
// Ens Name
var replacePatternENS = /\b[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.eth\b(?!\.\w)/g;
replacedText = replacedText.replace(replacePatternENS, function(match) {
return "<a href='//send-via-personal-chat//" + match + "'>" + match + "</a>";
});
}
return XSS.filterXSS(replacedText) return XSS.filterXSS(replacedText)
} }

View File

@ -45,6 +45,8 @@ StackLayout {
property bool communitySettingsDisabled property bool communitySettingsDisabled
property bool sendViaPersonalChatEnabled
property var emojiPopup property var emojiPopup
property var stickersPopup property var stickersPopup
signal profileButtonClicked() signal profileButtonClicked()
@ -158,6 +160,7 @@ StackLayout {
root.sectionItemModel.memberRole === Constants.memberRole.admin || root.sectionItemModel.memberRole === Constants.memberRole.admin ||
root.sectionItemModel.memberRole === Constants.memberRole.tokenMaster root.sectionItemModel.memberRole === Constants.memberRole.tokenMaster
hasViewOnlyPermissions: root.permissionsStore.viewOnlyPermissionsModel.count > 0 hasViewOnlyPermissions: root.permissionsStore.viewOnlyPermissionsModel.count > 0
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
hasUnrestrictedViewOnlyPermission: { hasUnrestrictedViewOnlyPermission: {
viewOnlyUnrestrictedPermissionHelper.revision viewOnlyUnrestrictedPermissionHelper.revision

View File

@ -56,6 +56,8 @@ Item {
readonly property bool isUserAdded: !!root.contactDetails && root.contactDetails.isContactRequestSent readonly property bool isUserAdded: !!root.contactDetails && root.contactDetails.isContactRequestSent
property bool amISectionAdmin: false property bool amISectionAdmin: false
property bool sendViaPersonalChatEnabled
signal openStickerPackPopup(string stickerPackId) signal openStickerPackPopup(string stickerPackId)
// This function is called once `1:1` or `group` chat is created. // This function is called once `1:1` or `group` chat is created.
@ -238,6 +240,7 @@ Item {
stickersPopup: root.stickersPopup stickersPopup: root.stickersPopup
stickersLoaded: root.stickersLoaded stickersLoaded: root.stickersLoaded
isBlocked: model.blocked isBlocked: model.blocked
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
onOpenStickerPackPopup: { onOpenStickerPackPopup: {
root.openStickerPackPopup(stickerPackId) root.openStickerPackPopup(stickerPackId)
} }

View File

@ -52,6 +52,8 @@ ColumnLayout {
chatSectionModule: root.rootStore.chatCommunitySectionModule chatSectionModule: root.rootStore.chatCommunitySectionModule
} }
property bool sendViaPersonalChatEnabled
signal showReplyArea(messageId: string) signal showReplyArea(messageId: string)
signal forceInputFocus() signal forceInputFocus()
@ -90,6 +92,7 @@ ColumnLayout {
isOneToOne: root.chatType === Constants.chatType.oneToOne isOneToOne: root.chatType === Constants.chatType.oneToOne
isChatBlocked: root.isBlocked || !root.isUserAllowedToSendMessage isChatBlocked: root.isBlocked || !root.isUserAllowedToSendMessage
channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "") channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "")
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
onShowReplyArea: (messageId, senderId) => { onShowReplyArea: (messageId, senderId) => {
root.showReplyArea(messageId) root.showReplyArea(messageId)
} }

View File

@ -45,6 +45,8 @@ Item {
property bool isChatBlocked: false property bool isChatBlocked: false
property bool isOneToOne: false property bool isOneToOne: false
property bool sendViaPersonalChatEnabled
signal openStickerPackPopup(string stickerPackId) signal openStickerPackPopup(string stickerPackId)
signal showReplyArea(string messageId, string author) signal showReplyArea(string messageId, string author)
signal editModeChanged(bool editModeOn) signal editModeChanged(bool editModeOn)
@ -276,6 +278,8 @@ Item {
isChatBlocked: root.isChatBlocked isChatBlocked: root.isChatBlocked
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
chatId: root.chatId chatId: root.chatId
messageId: model.id messageId: model.id
communityId: model.communityId communityId: model.communityId

View File

@ -72,6 +72,8 @@ StatusSectionLayout {
property var assetsModel property var assetsModel
property var collectiblesModel property var collectiblesModel
property bool sendViaPersonalChatEnabled
readonly property bool contentLocked: { readonly property bool contentLocked: {
if (!rootStore.chatCommunitySectionModule.isCommunity()) { if (!rootStore.chatCommunitySectionModule.isCommunity()) {
return false return false
@ -228,6 +230,7 @@ StatusSectionLayout {
viewAndPostHoldingsModel: root.viewAndPostPermissionsModel viewAndPostHoldingsModel: root.viewAndPostPermissionsModel
canPost: !root.rootStore.chatCommunitySectionModule.isCommunity() || root.canPost canPost: !root.rootStore.chatCommunitySectionModule.isCommunity() || root.canPost
amISectionAdmin: root.amISectionAdmin amISectionAdmin: root.amISectionAdmin
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
onOpenStickerPackPopup: { onOpenStickerPackPopup: {
Global.openPopup(statusStickerPackClickPopup, {packId: stickerPackId, store: root.stickersPopup.store} ) Global.openPopup(statusStickerPackClickPopup, {packId: stickerPackId, store: root.stickersPopup.store} )
} }

View File

@ -4,4 +4,5 @@ QtObject {
property bool connectorEnabled property bool connectorEnabled
property bool dappsEnabled property bool dappsEnabled
property bool swapEnabled property bool swapEnabled
property bool sendViaPersonalChatEnabled
} }

View File

@ -87,6 +87,7 @@ Item {
connectorEnabled: featureFlags ? featureFlags.connectorEnabled : false connectorEnabled: featureFlags ? featureFlags.connectorEnabled : false
dappsEnabled: featureFlags ? featureFlags.dappsEnabled : false dappsEnabled: featureFlags ? featureFlags.dappsEnabled : false
swapEnabled: featureFlags ? featureFlags.swapEnabled : false swapEnabled: featureFlags ? featureFlags.swapEnabled : false
sendViaPersonalChatEnabled: featureFlags ? featureFlags.sendViaPersonalChatEnabled : false
} }
required property bool isCentralizedMetricsEnabled required property bool isCentralizedMetricsEnabled
@ -1352,6 +1353,7 @@ Item {
currencyStore: appMain.currencyStore currencyStore: appMain.currencyStore
emojiPopup: statusEmojiPopup.item emojiPopup: statusEmojiPopup.item
stickersPopup: statusStickersPopupLoader.item stickersPopup: statusStickersPopupLoader.item
sendViaPersonalChatEnabled: featureFlagsStore.sendViaPersonalChatEnabled
onProfileButtonClicked: { onProfileButtonClicked: {
Global.changeAppSectionBySectionType(Constants.appSection.profile); Global.changeAppSectionBySectionType(Constants.appSection.profile);

View File

@ -131,6 +131,8 @@ Loader {
property bool hasMention: false property bool hasMention: false
property bool sendViaPersonalChatEnabled
property bool stickersLoaded: false property bool stickersLoaded: false
property string sticker property string sticker
property int stickerPack: -1 property int stickerPack: -1
@ -712,6 +714,7 @@ Loader {
disableEmojis: !d.addReactionAllowed disableEmojis: !d.addReactionAllowed
hideMessage: d.hideMessage hideMessage: d.hideMessage
linkAddressAndEnsName: root.sendViaPersonalChatEnabled
overrideBackground: root.placeholderMessage overrideBackground: root.placeholderMessage
profileClickable: !root.isDiscordMessage profileClickable: !root.isDiscordMessage
@ -728,6 +731,12 @@ Loader {
} }
onLinkActivated: { onLinkActivated: {
if (link.startsWith(Constants.sendViaChatPrefix)) {
const addressOrEns = link.replace(Constants.sendViaChatPrefix, "");
// TODO:: will be removed in the PRs to follow
Global.displayToastMessage(qsTr("TODO:: Send Via 1-1"), addressOrEns, "", false, 0, "")
return
}
if (link.startsWith('//')) { if (link.startsWith('//')) {
const pubkey = link.replace("//", ""); const pubkey = link.replace("//", "");
Global.openProfilePopup(pubkey) Global.openProfilePopup(pubkey)

View File

@ -987,6 +987,7 @@ QtObject {
readonly property string statusLinkPrefix: 'https://status.im/' readonly property string statusLinkPrefix: 'https://status.im/'
readonly property string statusHelpLinkPrefix: `https://status.app/help/` readonly property string statusHelpLinkPrefix: `https://status.app/help/`
readonly property string downloadLink: "https://status.im/get" readonly property string downloadLink: "https://status.im/get"
readonly property string sendViaChatPrefix: '//send-via-personal-chat//'
readonly property int maxUploadFiles: 6 readonly property int maxUploadFiles: 6
readonly property double maxUploadFilesizeMB: 10 readonly property double maxUploadFilesizeMB: 10