pragma Singleton import QtQuick 2.15 import shared 1.0 import StatusQ 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 as StatusQUtils QtObject { readonly property int maxImgSizeBytes: Constants.maxUploadFilesizeMB * 1048576 /* 1 MB in bytes */ readonly property int communityIdLength: 68 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 && value.startsWith('0x') } function getCommunityIdFromFullChatId(fullChatId) { return fullChatId.substr(0, communityIdLength) } function getChannelUuidFromFullChatId(fullChatId) { return fullChatId.substr(communityIdLength, fullChatId.length) } function isValidETHNamePrefix(value) { return !(value.trim() === "" || value.endsWith(".") || value.indexOf("..") > -1) } function isAddress(value) { return startsWith0x(value) && isHex(value) && value.length === 42 } 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 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 getStyledLink(linkText, linkUrl, hoveredLink, textColor = Theme.palette.directColor1, linkColor = Theme.palette.primaryColor1) { return `` + `${linkText}` } function isMnemonic(value) { if(!value.match(/^([a-z\s]+)$/)){ return false; } return 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 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) } function isValidChannelName(channelName) { return (/^[a-z0-9\-]+$/.test(channelName)) } function isURL(text) { return (/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6})?\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/.test(text)) } function isURLWithOptionalProtocol(text) { return (/^(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/.test(text)) } function isHexColor(c) { return (/^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/i.test(c)) } function isSpace(c) { return (/( |\t|\n|\r)/.test(c)) } function getTick(wordCount) { return (wordCount === 12 || wordCount === 15 || wordCount === 18 || wordCount === 21 || wordCount === 24) ? "✓ " : ""; } function isValidNumberOfWords(wordCount) { return !!getTick(wordCount); } function countWords(text) { if (text.trim() === "") return 0; return text.trim().replace(/ +/g, " ").split(" ").length; } function seedPhraseValidWordCount(text) { return isValidNumberOfWords(countWords(text)) } /** * Returns text in the format "✓ 12 words" for seed phrases input boxes */ function seedPhraseWordCountText(text) { const wordCount = countWords(text); return getTick(wordCount) + qsTr("%n word(s)", "", wordCount) } function uuid() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 5) } function validatePasswords(item, firstPasswordField, repeatPasswordField) { switch (item) { case "first": if (firstPasswordField.text === "") { return [false, qsTr("You need to enter a password")]; } else if (firstPasswordField.text.length < Constants.minPasswordLength) { return [false, qsTr("Password needs to be %n character(s) or more", "", Constants.minPasswordLength)]; } return [true, ""]; case "repeat": if (repeatPasswordField.text === "") { return [false, qsTr("You need to repeat your password")]; } else if (repeatPasswordField.text !== firstPasswordField.text) { return [false, qsTr("Passwords don't match")]; } return [true, ""]; default: return [false, ""]; } } function validatePINs(item, firstPINField, repeatPINField) { switch (item) { case "first": if (firstPINField.pinInput === "") { return [false, qsTr("You need to enter a PIN")]; } else if (!/^\d+$/.test(firstPINField.pinInput)) { return [false, qsTr("The PIN must contain only digits")]; } else if (firstPINField.pinInput.length != Constants.keycard.general.keycardPinLength) { return [false, qsTr("The PIN must be exactly %n digit(s)", "", Constants.keycard.general.keycardPinLength)]; } return [true, ""]; case "repeat": if (repeatPINField.pinInput === "") { return [false, qsTr("You need to repeat your PIN")]; } else if (repeatPINField.pinInput !== firstPINField.pinInput) { return [false, qsTr("PINs don't match")]; } return [true, ""]; default: return [false, ""]; } } function getHostname(url) { const rgx = /\:\/\/(?:[a-zA-Z0-9\-]*\.{1,}){1,}[a-zA-Z0-9]*/i const matches = rgx.exec(url) if (!matches || !matches.length) { if (url.includes(Constants.deepLinkPrefix)) { return Constants.deepLinkPrefix } return "" } return matches[0].substring(3) } function isStatusDeepLink(link) { return link.includes(Constants.deepLinkPrefix) || link.includes(Constants.externalStatusLink) } function removeGifUrls(message) { return message.replace(/(?:https?|ftp):\/\/[\n\S]*(\.gif)+/gm, ''); } function isValidDragNDropImage(url) { return url.startsWith(Constants.dataImagePrefix) || UrlUtils.isValidImageUrl(url, Constants.acceptedDragNDropImageExtensions) } function isFilesizeValid(img) { if (img.startsWith(Constants.dataImagePrefix)) { return img.length < maxImgSizeBytes } const size = UrlUtils.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 %n character(s)", "", limit).arg(fieldName) } 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 %n character(s) long", "", errors.minLength.min) } } return "" } /* Validation section end */ 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 getChatKeyFromShareLink(link) { let index = link.lastIndexOf("/u/") if (index === -1) { return link } return link.substring(index + 3) } function getElidedPk(publicKey) { if (publicKey === "") { return "" } return StatusQUtils.Utils.elideText(publicKey, 3, 6) } function getElidedCommunityPK(publicKey) { if (publicKey === "") { return "" } return StatusQUtils.Utils.elideText(publicKey, 16) } function elideIfTooLong(str, maxLength) { return (str.length > maxLength) ? str.substr(0, maxLength-4) + '...' : str; } 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.profile: return qsTr("Settings") case Constants.appSection.node: return qsTr("Node Management") case Constants.appSection.communitiesPortal: return qsTr("Discover Communities") 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 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 watched address") case Constants.appTranslatableConstants.addAccountLabelWatchOnlyAccount: return qsTr("Watched address") 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 watched address") } // 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", "short for account")).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 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.toString().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) { if(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 } } return Theme.palette.customisationColors.blue } function getColorIndexForId(colorId) { let color = getColorForId(colorId) for (let i = 0; i < Theme.palette.customisationColorsArray.length; i++) { if(Theme.palette.customisationColorsArray[i] === color) { return i } } return 0 } function getContrastingColor(color) { const hexcolor = color.toString() const r = parseInt(hexcolor.substring(1,3), 16); const g = parseInt(hexcolor.substring(3,5), 16); const b = parseInt(hexcolor.substring(5,7), 16); const yiq = ((r*299)+(g*587)+(b*114))/1000; return (yiq >= 128) ? Theme.palette.black : Theme.palette.white; } function getPathForDisplay(path) { return path.split("/").join(" / ") } function getKeypairLocationColor(keypair) { return !keypair || keypair.migratedToKeycard || keypair.operability === Constants.keypair.operability.fullyOperable || keypair.operability === Constants.keypair.operability.partiallyOperable? Theme.palette.baseColor1 : Theme.palette.warningColor1 } function getEtherscanUrl(networkShortName, testnetMode, sepoliaEnabled, addressOrTx, isAddressNotTx) { const type = isAddressNotTx ? Constants.networkExplorerLinks.addressPath : Constants.networkExplorerLinks.txPath let link = Constants.networkExplorerLinks.etherscan if (testnetMode) { if (sepoliaEnabled) { link = Constants.networkExplorerLinks.sepoliaEtherscan } else { link = Constants.networkExplorerLinks.goerliEtherscan } } if (networkShortName === Constants.networkShortChainNames.arbitrum) { link = Constants.networkExplorerLinks.arbiscan if (testnetMode) { if (sepoliaEnabled) { link = Constants.networkExplorerLinks.sepoliaArbiscan } else { link = Constants.networkExplorerLinks.goerliArbiscan } } } else if (networkShortName === Constants.networkShortChainNames.optimism) { link = Constants.networkExplorerLinks.optimism if (testnetMode) { if (sepoliaEnabled) { link = Constants.networkExplorerLinks.sepoliaOptimism } else { link = Constants.networkExplorerLinks.goerliOptimism } } } return "%1/%2/%3".arg(link).arg(type).arg(addressOrTx) } // Etherscan URL for an address function getUrlForAddressOnNetwork(networkShortName, testnetMode, sepoliaEnabled, address) { return getEtherscanUrl(networkShortName, testnetMode, sepoliaEnabled, address, true /* is address */) } // Etherscan URL for a transaction function getUrlForTxOnNetwork(networkShortName, testnetMode, sepoliaEnabled, tx) { return getEtherscanUrl(networkShortName, testnetMode, sepoliaEnabled, tx, false /* is TX */) } // 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) } function getUrlStatus(url) { // TODO: Analyse and implement // #15331 return true } function toBase64(buffer) { const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; const bufferView = new Uint8Array(buffer); let result = ""; let i; for (i = 0; i < bufferView.length - 2; i += 3) { const chunk = (bufferView[i] << 16) | (bufferView[i + 1] << 8) | bufferView[i + 2]; result += base64Chars[(chunk >> 18) & 0x3F] + base64Chars[(chunk >> 12) & 0x3F] + base64Chars[(chunk >> 6) & 0x3F] + base64Chars[chunk & 0x3F]; } if (bufferView.length % 3 === 1) { const chunk = bufferView[i] << 16; result += base64Chars[(chunk >> 18) & 0x3F] + base64Chars[(chunk >> 12) & 0x3F] + "=="; } else if (bufferView.length % 3 === 2) { const chunk = (bufferView[i] << 16) | (bufferView[i + 1] << 8); result += base64Chars[(chunk >> 18) & 0x3F] + base64Chars[(chunk >> 12) & 0x3F] + base64Chars[(chunk >> 6) & 0x3F] + "="; } return result; } function fetchImageBase64(url, callback) { let xhr = new XMLHttpRequest(); xhr.open("GET", url, true); xhr.responseType = "arraybuffer"; xhr.onload = function() { if (xhr.status === 200) { const base64Image = toBase64(xhr.response); const mimeType = xhr.getResponseHeader("Content-Type") || "image/png"; callback(`data:${mimeType};base64,${base64Image}`); } else { callback(""); } } xhr.send(); } // BACKEND DEPENDENT PART // // Methods and properties below are intended to be refactored in various // ways to finally make that singleton fully stateless and backend-independent. property var mainModuleInst: typeof mainModule !== "undefined" ? mainModule : null property var sharedUrlsModuleInst: typeof sharedUrlsModule !== "undefined" ? sharedUrlsModule : null property var globalUtilsInst: typeof globalUtils !== "undefined" ? globalUtils : null property var communitiesModuleInst: typeof communitiesModule !== "undefined" ? communitiesModule : null function isChatKey(value) { return (startsWith0x(value) && isHex(value) && value.length === 132) || globalUtilsInst.isCompressedPubKey(value) } function isCommunityPublicKey(value) { return (startsWith0x(value) && isHex(value) && value.length === communityIdLength) || globalUtilsInst.isCompressedPubKey(value) } function isCompressedPubKey(pubKey) { return globalUtilsInst.isCompressedPubKey(pubKey) } function isAlias(name) { return globalUtilsInst.isAlias(name) } function getContactDetailsAsJson(publicKey, getVerificationRequest=true, getOnlineStatus=false, includeDetails=false) { const defaultValue = { defaultDisplayName: "", optionalName: "", icon: "", isCurrentUser: "", colorId: "", colorHash: "", displayName: "", publicKey: publicKey, name: "", ensVerified: false, alias: "", lastUpdated: 0, lastUpdatedLocally: 0, localNickname: "", thumbnailImage: "", largeImage: "", isContact: false, isBlocked: false, isContactRequestReceived: false, isContactRequestSent: false, isSyncing: false, removed: false, trustStatus: Constants.trustStatus.unknown, contactRequestState: Constants.ContactRequestState.None, verificationStatus: Constants.verificationStatus.unverified, incomingVerificationStatus: Constants.verificationStatus.unverified, socialLinks: [], bio: "", onlineStatus: Constants.onlineStatus.inactive } if (!mainModuleInst || !publicKey) return defaultValue const jsonObj = mainModuleInst.getContactDetailsAsJson(publicKey, getVerificationRequest, getOnlineStatus, includeDetails) 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 colorForPubkey(publicKey) { const pubKeyColorId = colorIdForPubkey(publicKey) return colorForColorId(pubKeyColorId) } function getCommunityShareLink(communityId) { if (communityId === "") { return "" } return communitiesModuleInst.shareCommunityUrlWithData(communityId) } function getCommunityChannelShareLink(communityId, channelId) { if (communityId === "" || channelId === "") return "" return communitiesModuleInst.shareCommunityChannelUrlWithData(communityId, channelId) } function getCommunityChannelShareLinkWithChatId(chatId) { const communityId = getCommunityIdFromFullChatId(chatId) const channelId = getChannelUuidFromFullChatId(chatId) return getCommunityChannelShareLink(communityId, channelId) } 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 getCommunityDataFromSharedLink(link) { const index = link.lastIndexOf("/c/") if (index === -1) return null const communityDataString = sharedUrlsModuleInst.parseCommunitySharedUrl(link) try { return JSON.parse(communityDataString) } catch (e) { console.warn("Error while parsing community data from url:", e.message) return null } } function changeCommunityKeyCompression(communityKey) { return globalUtilsInst.changeCommunityKeyCompression(communityKey) } function getCompressedPk(publicKey) { if (publicKey === "") { return "" } if (!isChatKey(publicKey)) return publicKey return globalUtilsInst.getCompressedPk(publicKey) } function getElidedCompressedPk(publicKey) { if (publicKey === "") { return "" } let compressedPk = getCompressedPk(publicKey) return getElidedPk(compressedPk) } function parseContactUrl(link) { let index = link.lastIndexOf("/u/") if (index === -1) { index = link.lastIndexOf("/u#") } if (index === -1) return null const contactDataString = sharedUrlsModuleInst.parseContactSharedUrl(link) try { return JSON.parse(contactDataString) } catch (e) { return null } } function getKeypairLocation(keypair, fromAccountDetailsView) { if (!keypair || keypair.pairType === Constants.keypair.type.watchOnly) { return "" } let profileTitle = "" if (keypair.pairType === Constants.keypair.type.profile) { profileTitle = Utils.getElidedCompressedPk(keypair.pubKey) + Constants.settingsSection.dotSepString } if (keypair.migratedToKeycard) { return profileTitle + qsTr("On Keycard") } if (keypair.operability === Constants.keypair.operability.fullyOperable || keypair.operability === Constants.keypair.operability.partiallyOperable) { return profileTitle + qsTr("On device") } if (keypair.operability === Constants.keypair.operability.nonOperable) { if (fromAccountDetailsView) { return qsTr("Requires import") } else if (keypair.syncedFrom === Constants.keypair.syncedFrom.backup) { if (keypair.pairType === Constants.keypair.type.seedImport || keypair.pairType === Constants.keypair.type.privateKeyImport) { return qsTr("Restored from backup. Import key pair to use derived accounts.") } } return qsTr("Import key pair to use derived accounts") } return "" } function addTimestampToURL(url) { return globalUtilsInst.addTimestampToURL(url) } // Returns true if the provided displayName occurs in community members function isDisplayNameDupeOfCommunityMember(displayName) { if (!communitiesModuleInst) return false if (displayName === "") return false const myDisplayName = Global.userProfile ? Global.userProfile.name : "" if (displayName === myDisplayName) return false return communitiesModuleInst.isDisplayNameDupeOfCommunityMember(displayName) } }