pragma Singleton
import QtQuick 2.13
import shared 1.0
import StatusQ 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
QtObject {
property var mainModuleInst: typeof mainModule !== "undefined" ? mainModule : null
property var globalUtilsInst: typeof globalUtils !== "undefined" ? globalUtils : null
readonly property int maxImgSizeBytes: Constants.maxUploadFilesizeMB * 1048576 /* 1 MB in bytes */
function isDigit(value) {
return /^\d$/.test(value);
}
function isHex(value) {
return /^(-0x|0x)?[0-9a-fA-F]*$/i.test(value)
}
function startsWith0x(value) {
return value.startsWith('0x')
}
function isChatKey(value) {
return (startsWith0x(value) && isHex(value) && value.length === 132) || globalUtilsInst.isCompressedPubKey(value)
}
function isCommunityPublicKey(value) {
return (startsWith0x(value) && isHex(value) && value.length === 68) || globalUtilsInst.isCompressedPubKey(value)
}
function isCompressedPubKey(pubKey) {
return globalUtilsInst.isCompressedPubKey(pubKey)
}
function isValidETHNamePrefix(value) {
return !(value.trim() === "" || value.endsWith(".") || value.indexOf("..") > -1)
}
function isAddress(value) {
return startsWith0x(value) && isHex(value) && value.length === 42
}
function isValidAddressWithChainPrefix(value) {
return value.match(/^(([a-zA-Z]{3,5}:)*)?(0x[a-fA-F0-9]{40})$/)
}
function getChainsPrefix(address) {
// matchAll is not supported by QML JS engine
return address.match(/([a-zA-Z]{3,5}:)*/)[0].split(':').filter(e => !!e)
}
function isLikelyEnsName(text) {
return text.startsWith("@") || !isLikelyAddress(text)
}
function isLikelyAddress(text) {
return text.includes(":") || text.includes('0x')
}
function richColorText(text, color) {
return "" + text + ""
}
function splitToChainPrefixAndAddress(input) {
const addressIdx = input.indexOf('0x')
if (addressIdx < 0)
return { prefix: input, address: "" }
return { prefix: input.substring(0, addressIdx), address: input.substring(addressIdx) }
}
function isPrivateKey(value) {
return isHex(value) && ((startsWith0x(value) && value.length === 66) ||
(!startsWith0x(value) && value.length === 64))
}
function getCurrentThemeAccountColor(color) {
const upperCaseColor = color.toUpperCase()
if (Style.current.accountColors.indexOf(upperCaseColor) > -1) {
return upperCaseColor
}
let colorIndex
if (Style.current.name === Constants.lightThemeName) {
colorIndex = Style.darkTheme.accountColors.indexOf(upperCaseColor)
} else {
colorIndex = Style.lightTheme.accountColors.indexOf(upperCaseColor)
}
if (colorIndex === -1) {
// Unknown color
return false
}
return Style.current.accountColors[colorIndex]
}
function validLink(link) {
if (link.length === 0) {
return false
}
var regex = /[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/
return regex.test(link)
}
function getLinkStyle(link, hoveredLink, textColor) {
return `` +
`${link}`
}
function isMnemonic(value) {
if(!value.match(/^([a-z\s]+)$/)){
return false;
}
return Utils.seedPhraseValidWordCount(value);
}
function compactAddress(addr, numberOfChars) {
if(addr.length <= 5 + (numberOfChars * 2)){ // 5 represents these chars 0x...
return addr;
}
return addr.substring(0, 2 + numberOfChars) + "..." + addr.substring(addr.length - numberOfChars);
}
function toLocaleString(val, locale, options) {
if (typeof(val) === "object") {
console.log("Wrong type for val: " + JSON.stringify(val))
return NaN
}
return NumberPolyFill.toLocaleString(val, locale, options)
}
function isOnlyEmoji(inputText) {
var emoji_regex = /^(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c[\ude32-\ude3a]|[\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff]|\s)+$/;
return emoji_regex.test(inputText);
}
function isValidAddress(inputValue) {
return inputValue !== "0x" && /^0x[a-fA-F0-9]{40}$/.test(inputValue)
}
function isValidEns(inputValue) {
if (!inputValue) {
return false
}
const isEmail = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(inputValue)
const isDomain = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/.test(inputValue)
return isEmail || isDomain || (inputValue.startsWith("@") && inputValue.length > 1)
}
/**
* Removes trailing zeros from a string-representation of a number. Throws
* if parameter is not a string
*/
function stripTrailingZeros(strNumber) {
if (!(typeof strNumber === "string")) {
try {
strNumber = strNumber.toString()
} catch(e) {
throw "[Utils.stripTrailingZeros] input parameter must be a string"
}
}
return strNumber.replace(/(\.[0-9]*[1-9])0+$|\.0*$/,'$1')
}
/**
* Removes starting zeros from a string-representation of a number. Throws
* if parameter is not a string
*/
function stripStartingZeros(strNumber) {
if (!(typeof strNumber === "string")) {
try {
strNumber = strNumber.toString()
} catch(e) {
throw "[Utils.stripStartingZeros] input parameter must be a string"
}
}
return strNumber.replace(/^(0*)([0-9\.]+)/, "$2")
}
function setColorAlpha(color, alpha) {
return Qt.hsla(color.hslHue, color.hslSaturation, color.hslLightness, alpha)
}
// To-do move to Wallet Store, this should not be under Utils.
function findAssetByChainAndSymbol(chainIdToFind, assets, symbolToFind) {
for(var i=0; i url.toLowerCase().includes(ext))
}
function removeGifUrls(message) {
return message.replace(/(?:https?|ftp):\/\/[\n\S]*(\.gif)+/gm, '');
}
function isValidDragNDropImage(url) {
return url.startsWith(Constants.dataImagePrefix) ||
QClipboardProxy.isValidImageUrl(url, Constants.acceptedDragNDropImageExtensions)
}
function isFilesizeValid(img) {
if (img.startsWith(Constants.dataImagePrefix)) {
return img.length < maxImgSizeBytes
}
const size = QClipboardProxy.getFileSize(img)
return size <= maxImgSizeBytes
}
function deduplicate(array) {
return Array.from(new Set(array))
}
function hasUpperCaseLetter(str) {
return (/[A-Z]/.test(str))
}
function convertSpacesToDashes(str)
{
return str.replace(/ /g, "-")
}
/* Validation section start */
enum Validate {
NoEmpty = 0x01,
TextLength = 0x02,
TextHexColor = 0x04,
TextLowercaseLettersNumberAndDashes = 0x08
}
function validateAndReturnError(str, validation, fieldName = "field", limit = 0)
{
let errMsg = ""
if(validation & Utils.Validate.NoEmpty && str === "") {
errMsg = qsTr("You need to enter a %1").arg(fieldName)
}
if(validation & Utils.Validate.TextLength && str.length > limit) {
errMsg = qsTr("The %1 cannot exceed %2 characters").arg(fieldName, limit)
}
if(validation & Utils.Validate.TextHexColor && !isHexColor(str)) {
errMsg = qsTr("Must be an hexadecimal color (eg: #4360DF)")
}
if(validation & Utils.Validate.TextLowercaseLettersNumberAndDashes && !isValidChannelName(str)) {
errMsg = qsTr("Use only lowercase letters (a to z), numbers & dashes (-). Do not use chat keys.")
}
return errMsg
}
function getErrorMessage(errors, fieldName) {
if (errors) {
if (errors.minLength) {
return errors.minLength.min === 1 ?
qsTr("You need to enter a %1").arg(fieldName) :
qsTr("Value has to be at least %1 characters long").arg(errors.minLength.min)
}
}
return ""
}
/* Validation section end */
function getContactDetailsAsJson(publicKey, getVerificationRequest=true) {
const defaultValue = {
displayName: "",
displayIcon: "",
publicKey: publicKey,
name: "",
ensVerified: false,
alias: "",
lastUpdated: 0,
lastUpdatedLocally: 0,
localNickname: "",
thumbnailImage: "",
largeImage: "",
isContact: false,
isAdded: false,
isBlocked: false,
requestReceived: false,
isSyncing: false,
removed: false,
trustStatus: Constants.trustStatus.unknown,
verificationStatus: Constants.verificationStatus.unverified,
incomingVerificationStatus: Constants.verificationStatus.unverified
}
if (!mainModuleInst || !publicKey)
return defaultValue
const jsonObj = mainModuleInst.getContactDetailsAsJson(publicKey, getVerificationRequest)
try {
return JSON.parse(jsonObj)
}
catch (e) {
// This log is available only in debug mode, if it's annoying we can remove it
console.warn("error parsing contact details for public key: ", publicKey, " error: ", e.message)
return defaultValue
}
}
function isEnsVerified(publicKey) {
if (publicKey === "" || !isChatKey(publicKey) )
return false
if (!mainModuleInst)
return false
return mainModuleInst.isEnsVerified(publicKey)
}
function getEmojiHashAsJson(publicKey) {
if (publicKey === "" || !isChatKey(publicKey)) {
return ""
}
let jsonObj = globalUtilsInst.getEmojiHashAsJson(publicKey)
return JSON.parse(jsonObj)
}
function getColorHashAsJson(publicKey, skipEnsVerification=false) {
if (publicKey === "" || !isChatKey(publicKey))
return
if (skipEnsVerification) // we know already the user is ENS verified -> no color ring
return
if (isEnsVerified(publicKey)) // ENS verified -> no color ring
return
let jsonObj = globalUtilsInst.getColorHashAsJson(publicKey)
return JSON.parse(jsonObj)
}
function colorIdForPubkey(publicKey) {
if (publicKey === "" || !isChatKey(publicKey)) {
return 0
}
return globalUtilsInst.getColorId(publicKey)
}
function colorForColorId(colorId) {
if (colorId < 0 || colorId >= Theme.palette.userCustomizationColors.length) {
console.warn("Utils.colorForColorId : colorId is out of bounds")
return StatusColors.colors['blue']
}
return Theme.palette.userCustomizationColors[colorId]
}
function colorForPubkey(publicKey) {
const pubKeyColorId = colorIdForPubkey(publicKey)
return colorForColorId(pubKeyColorId)
}
function getCommunityShareLink(communityId, elided = false) {
if (communityId === "") {
return ""
}
let compressedPk = communityId
if (!globalUtilsInst.isCompressedPubKey(compressedPk)) {
compressedPk = globalUtilsInst.changeCommunityKeyCompression(compressedPk)
}
return Constants.communityLinkPrefix +
(elided ? StatusQUtils.Utils.elideText(compressedPk, 4, 2) : compressedPk)
}
function getChatKeyFromShareLink(link) {
let index = link.lastIndexOf("/u/")
if (index === -1) {
return link
}
return link.substring(index + 3)
}
function getCommunityIdFromShareLink(link) {
let index = link.lastIndexOf("/c/")
if (index === -1) {
return ""
}
const communityKey = link.substring(index + 3)
if (globalUtilsInst.isCompressedPubKey(communityKey)) {
// is zQ.., need to be converted to standard compression
return globalUtilsInst.changeCommunityKeyCompression(communityKey)
}
return communityKey
}
function changeCommunityKeyCompression(communityKey) {
return globalUtilsInst.changeCommunityKeyCompression(communityKey)
}
function getCompressedPk(publicKey) {
if (publicKey === "") {
return ""
}
if (!isChatKey(publicKey))
return publicKey
return globalUtilsInst.getCompressedPk(publicKey)
}
function getElidedPk(publicKey) {
if (publicKey === "") {
return ""
}
return StatusQUtils.Utils.elideText(publicKey, 5, 3)
}
function getElidedCommunityPK(publicKey) {
if (publicKey === "") {
return ""
}
return StatusQUtils.Utils.elideText(publicKey, 16)
}
function getElidedCompressedPk(publicKey) {
if (publicKey === "") {
return ""
}
let compressedPk = getCompressedPk(publicKey)
return getElidedPk(compressedPk, 6, 3)
}
function elideIfTooLong(str, maxLength) {
return (str.length > maxLength) ? str.substr(0, maxLength-4) + '...' : str;
}
function escapeHtml(unsafeStr) {
return globalUtilsInst.escapeHtml(unsafeStr)
}
function plainText(text) {
return globalUtilsInst.plainText(text)
}
function isInvalidPasswordMessage(msg) {
return (
msg.includes("could not decrypt key with given password") ||
msg.includes("invalid password")
);
}
function isInvalidPrivateKey(msg) {
return msg.includes("invalid private key");
}
function isInvalidPath(msg) {
return msg.includes(Constants.wrongDerivationPathError)
}
function accountAlreadyExistsError(msg) {
return msg.includes(Constants.existingAccountError)
}
// See also: backend/interpret/cropped_image.nim
function getImageAndCropInfoJson(imgPath, cropRect) {
return JSON.stringify({imagePath: String(imgPath).replace("file://", ""), cropRect: cropRect})
}
// handle translations for section names coming from app_sections_config.nim
function translatedSectionName(sectionType, fallback) {
switch(sectionType) {
case Constants.appSection.chat:
return qsTr("Messages")
case Constants.appSection.wallet:
return qsTr("Wallet")
case Constants.appSection.browser:
return qsTr("Browser")
case Constants.appSection.profile:
return qsTr("Settings")
case Constants.appSection.node:
return qsTr("Node Management")
case Constants.appSection.communitiesPortal:
return qsTr("Communities Portal")
default:
return fallback
}
}
function getFontSizeBasedOnLetterCount(text) {
if(text.length >= 12)
return 18
if(text.length >= 10)
return 24
if(text.length > 6)
return 28
else
return 34
}
function getTimerString(timeInSecs) {
let result = ""
const hour = Math.floor(timeInSecs/60/60)
const mins = Math.floor(timeInSecs/60%60)
const secs = Math.floor(timeInSecs%60)
if(hour > 0 )
result += qsTr(" %n hour(s) ", "", hour)
if(mins > 0)
result += qsTr(" %n min(s) ", "", mins)
if(secs > 0)
result += qsTr(" %n sec(s) ", "", secs)
return result
}
function appTranslation(key) {
switch(key) {
case Constants.appTranslatableConstants.loginAccountsListAddNewUser:
return qsTr("Add new user")
case Constants.appTranslatableConstants.loginAccountsListAddExistingUser:
return qsTr("Add existing Status user")
case Constants.appTranslatableConstants.loginAccountsListLostKeycard:
return qsTr("Lost Keycard")
case Constants.appTranslatableConstants.addAccountLabelNewWatchOnlyAccount:
return qsTr("New watch-only account")
case Constants.appTranslatableConstants.addAccountLabelWatchOnlyAccount:
return qsTr("Watch-only account")
case Constants.appTranslatableConstants.addAccountLabelExisting:
return qsTr("Existing")
case Constants.appTranslatableConstants.addAccountLabelImportNew:
return qsTr("Import new")
case Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey:
return qsTr("Add new master key")
case Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc:
return qsTr("Add watch-only account")
}
// special handling because on an index attached to the constant
if (key.startsWith(Constants.appTranslatableConstants.keycardAccountNameOfUnknownWalletAccount)) {
let num = key.substring(Constants.appTranslatableConstants.keycardAccountNameOfUnknownWalletAccount.length)
return "%1%2".arg(qsTr("acc")).arg(num) //short name of an unknown (removed) wallet account
}
return key
}
function dropUserLinkPrefix(text) {
if (text.startsWith(Constants.userLinkPrefix))
text = text.slice(Constants.userLinkPrefix.length)
return text
}
function dropCommunityLinkPrefix(text) {
if (text.startsWith(Constants.communityLinkPrefix))
text = text.slice(Constants.communityLinkPrefix.length)
return text
}
function copyImageToClipboardByUrl(content) {
globalUtilsInst.copyImageToClipboardByUrl(content)
}
function downloadImageByUrl(url, path) {
globalUtilsInst.downloadImageByUrl(url, path)
}
function getHoveredColor(colorId) {
let isLightTheme = Theme.palette.name === Constants.lightThemeName
switch(colorId.toString().toUpperCase()) {
case Constants.walletAccountColors.primary.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.blue: Style.statusQLightTheme.customisationColors.blue
case Constants.walletAccountColors.purple.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.purple: Style.statusQLightTheme.customisationColors.purple
case Constants.walletAccountColors.orange.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.orange: Style.statusQLightTheme.customisationColors.orange
case Constants.walletAccountColors.army.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.army: Style.statusQLightTheme.customisationColors.army
case Constants.walletAccountColors.turquoise.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.turquoise: Style.statusQLightTheme.customisationColors.turquoise
case Constants.walletAccountColors.sky.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.sky: Style.statusQLightTheme.customisationColors.sky
case Constants.walletAccountColors.yellow.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.yellow: Style.statusQLightTheme.customisationColors.yellow
case Constants.walletAccountColors.pink.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.pink: Style.statusQLightTheme.customisationColors.pink
case Constants.walletAccountColors.copper.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.copper: Style.statusQLightTheme.customisationColors.copper
case Constants.walletAccountColors.camel.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.camel: Style.statusQLightTheme.customisationColors.camel
case Constants.walletAccountColors.magenta.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.customisationColors.magenta: Style.statusQLightTheme.customisationColors.magenta
case Constants.walletAccountColors.yinYang.toUpperCase():
return isLightTheme ? Theme.palette.getColor('blackHovered'): Theme.palette.getColor('grey4')
case Constants.walletAccountColors.undefinedAccount.toUpperCase():
return isLightTheme ? Style.statusQDarkTheme.baseColor1: Style.statusQLightTheme.baseColor1
default:
return getColorForId(colorId)
}
}
function getIdForColor(color){
let c = color.toUpperCase()
switch(c) {
case Theme.palette.customisationColors.blue.toString().toUpperCase():
return Constants.walletAccountColors.primary
case Theme.palette.customisationColors.purple.toString().toUpperCase():
return Constants.walletAccountColors.purple
case Theme.palette.customisationColors.orange.toString().toUpperCase():
return Constants.walletAccountColors.orange
case Theme.palette.customisationColors.army.toString().toUpperCase():
return Constants.walletAccountColors.army
case Theme.palette.customisationColors.turquoise.toString().toUpperCase():
return Constants.walletAccountColors.turquoise
case Theme.palette.customisationColors.sky.toString().toUpperCase():
return Constants.walletAccountColors.sky
case Theme.palette.customisationColors.yellow.toString().toUpperCase():
return Constants.walletAccountColors.yellow
case Theme.palette.customisationColors.pink.toString().toUpperCase():
return Constants.walletAccountColors.pink
case Theme.palette.customisationColors.copper.toString().toUpperCase():
return Constants.walletAccountColors.copper
case Theme.palette.customisationColors.camel.toString().toUpperCase():
return Constants.walletAccountColors.camel
case Theme.palette.customisationColors.magenta.toString().toUpperCase():
return Constants.walletAccountColors.magenta
case Theme.palette.customisationColors.yinYang.toString().toUpperCase():
return Constants.walletAccountColors.yinYang
case Theme.palette.baseColor1.toString().toUpperCase():
return Constants.walletAccountColors.undefinedAccount
default:
return Constants.walletAccountColors.primary
}
}
function getColorForId(colorId) {
switch(colorId.toUpperCase()) {
case Constants.walletAccountColors.primary.toUpperCase():
return Theme.palette.customisationColors.blue
case Constants.walletAccountColors.purple.toUpperCase():
return Theme.palette.customisationColors.purple
case Constants.walletAccountColors.orange.toUpperCase():
return Theme.palette.customisationColors.orange
case Constants.walletAccountColors.army.toUpperCase():
return Theme.palette.customisationColors.army
case Constants.walletAccountColors.turquoise.toUpperCase():
return Theme.palette.customisationColors.turquoise
case Constants.walletAccountColors.sky.toUpperCase():
return Theme.palette.customisationColors.sky
case Constants.walletAccountColors.yellow.toUpperCase():
return Theme.palette.customisationColors.yellow
case Constants.walletAccountColors.pink.toUpperCase():
return Theme.palette.customisationColors.pink
case Constants.walletAccountColors.copper.toUpperCase():
return Theme.palette.customisationColors.copper
case Constants.walletAccountColors.camel.toUpperCase():
return Theme.palette.customisationColors.camel
case Constants.walletAccountColors.magenta.toUpperCase():
return Theme.palette.customisationColors.magenta
case Constants.walletAccountColors.yinYang.toUpperCase():
return Theme.palette.customisationColors.yinYang
case Constants.walletAccountColors.undefinedAccount.toUpperCase():
return Theme.palette.baseColor1
default:
return Theme.palette.customisationColors.blue
}
}
// Leave this function at the bottom of the file as QT Creator messes up the code color after this
function isPunct(c) {
return /(!|\@|#|\$|%|\^|&|\*|\(|\)|\+|\||-|=|\\|{|}|[|]|"|;|'|<|>|\?|,|\.|\/)/.test(c)
}
}