From ea54b9b6e119ed95a4eaf72b29af242f24144ca2 Mon Sep 17 00:00:00 2001 From: Jarrad Hope Date: Wed, 2 Nov 2016 15:20:50 +0800 Subject: [PATCH] !js command in console chat --- .../react-native-status/android/build.gradle | 2 +- .../react-native-status/ios/RCTStatus/pom.xml | 2 +- resources/console.js | 1026 +++++++++++++++++ resources/status.js | 8 +- src/status_im/chat/handlers/commands.cljs | 26 +- src/status_im/chat/handlers/console.cljs | 41 +- src/status_im/chat/handlers/send_message.cljs | 3 +- src/status_im/chat/views/new_message.cljs | 8 +- src/status_im/commands/handlers/jail.cljs | 7 +- src/status_im/components/status.cljs | 8 +- src/status_im/handlers.cljs | 1 + 11 files changed, 1115 insertions(+), 17 deletions(-) diff --git a/modules/react-native-status/android/build.gradle b/modules/react-native-status/android/build.gradle index 21c7256cc8..fe7ea8297e 100644 --- a/modules/react-native-status/android/build.gradle +++ b/modules/react-native-status/android/build.gradle @@ -14,5 +14,5 @@ android { dependencies { compile 'com.facebook.react:react-native:+' - compile(group: 'status-im', name: 'status-go', version: 'rebase-to-1.5.4-unstable', ext: 'aar') + compile(group: 'status-im', name: 'status-go', version: 'local-storage', ext: 'aar') } diff --git a/modules/react-native-status/ios/RCTStatus/pom.xml b/modules/react-native-status/ios/RCTStatus/pom.xml index 2c1401282b..ce643ad596 100644 --- a/modules/react-native-status/ios/RCTStatus/pom.xml +++ b/modules/react-native-status/ios/RCTStatus/pom.xml @@ -25,7 +25,7 @@ status-im status-go-ios-simulator - rebase-to-1.5.4-unstable + local-storage zip true ./ diff --git a/resources/console.js b/resources/console.js index 84413c3366..3058561d5f 100644 --- a/resources/console.js +++ b/resources/console.js @@ -562,6 +562,1031 @@ I18n.translations = { } }; +var WEB3_UNIT = [ + 'kwei/ada', + 'mwei/babbage', + 'gwei/shannon', + 'szabo', + 'finney', + 'ether', + 'kether/grand/einstein', + 'mether', + 'gether', + 'tether' +]; + +// because web3 doesn't provide params or docs +var DOC_MAP = { + console: { + log : { + desc: 'Outputs a message to chat context.', + args: [{ + name: 'text', + type: 'String', + desc: 'message to output to chat context' + }] + } + }, + web3: { + // setProvider : ['provider'], // TODO + version: { + api: { + desc: 'The ethereum js api version.' + }, + node: { + desc: 'The client/node version.' + }, + network: { + desc: 'The network protocol version.' + }, + ethereum: { + desc: 'The ethereum protocol version.' + }, + whisper: { + desc: 'The whisper protocol version.' + }, + }, + isConnected: { + desc: 'Check if a connection to a node exists.', + args: [] + }, + currentProvider: { + desc: 'Will contain the current provider, if one is set. This can be used to check if mist etc. set already a provider.' + }, + reset: { + desc: 'Should be called to reset state of web3. Resets everything except manager. Uninstalls all filters. Stops polling.', + args: [{ + name: 'keepIsSyncing', + type: 'Boolean', + desc: 'If true it will uninstall all filters, but will keep the web3.eth.isSyncing() polls' + }] + }, + sha3: { + desc: 'Returns the Keccak-256 SHA3 of the given data.', + args: [{ + name: 'string', + type: 'String', + desc: 'The string to hash using the Keccak-256 SHA3 algorithm' + }, { + name: 'options', + type: 'Object', + optional: true, + desc: 'Set encoding to hex if the string to hash is encoded in hex. A leading 0x will be automatically ignored.' + }] + }, + toHex: { + desc: 'Converts any value into HEX', + args: [{ + name: 'mixed', + type: 'String|Number|Object|Array|BigNumber', + desc: 'The value to parse to HEX. If its an object or array it will be JSON.stringify first. If its a BigNumber it will make it the HEX value of a number.' + }] + }, + toAscii: { + desc: 'Converts a HEX string into a ASCII string.', + args: [{ + name: 'hexString', + type: 'String', + desc: 'A HEX string to be converted to ascii.' + }] + }, + fromAscii: { + desc: 'Converts any ASCII string to a HEX string.', + args: [{ + name: 'string', + type: 'String', + desc: 'An ASCII string to be converted to HEX.' + }, { + name: 'padding', + type: 'Number', + desc: 'The number of bytes the returned HEX string should have. ' + }] + }, + toDecimal: { + desc: 'Converts a HEX string to its number representation.', + args: [{ + name: 'hexString', + type: 'String', + desc: 'An HEX string to be converted to a number.' + }] + }, + fromDecimal: { + desc: 'Converts a number or number string to its HEX representation.', + args: [{ + name: 'number', + type: 'Number', + desc: 'A number to be converted to a HEX string.' + }] + }, + fromWei: { + desc: 'Converts a number of wei into an ethereum unit', + args: [{ + name: 'number', + type: 'Number|String|BigNumber', + desc: 'A number or BigNumber instance.' + }, { + name: 'unit', + type: 'string', + desc: 'One of the ether units' + }] + }, + toWei: { + desc: 'Converts an ethereum unit into wei', + args: [{ + name: 'number', + type: 'Number|String|BigNumber', + desc: 'A number or BigNumber instance.' + }, { + name: 'unit', + type: 'string', + desc: 'One of the ether units' + }] + }, + toBigNumber: { + desc: 'Converts a given number into a BigNumber instance', + args: [{ + name: 'numberOrHexString', + type: 'Number|String', + desc: 'A number, number string or HEX string of a number.' + }] + }, + net: { + listening: { + desc: 'Is node actively listening for network connections?' + }, + peerCount: { + desc: 'Returns the number of connected peers' + } + }, + isAddress: { + desc: '', + args: [{ + name: '', + type: 'string', + desc: 'hex string' + }], // TODO not in docs + }, + eth: { + defaultAccount: { + desc: 'The currently set default address' + }, + defaultBlock: { + desc: 'The default block number to use when querying a state.' + }, + syncing: { + desc: 'Returns the either a sync object, when the node is syncing or false.' + }, + isSyncing: { + desc: 'This convenience function calls the callback everytime a sync starts, updates and stops.', + args: [{ + name: 'callback', + type: 'Function', + desc: 'The callback will be fired with true when the syncing starts and with false when it stopped. While syncing it will return the syncing object: {startingBlock, currentBlock, highestBlock}' + }] + }, + coinbase: { + desc: 'Returns the coinbase address were the mining rewards go to.' + }, + mining: { + desc: 'Says whether the node is mining or not.' + }, + hashrate: { + desc: 'Returns the number of hashes per second that the node is mining with.' + }, + gasPrice: { + desc: 'Returns the current gas price. The gas price is determined by the x latest blocks median gas price' + }, + accounts: { + desc: 'Returns a list of accounts the node controls' + }, + blockNumber: { + desc: 'Returns the current block number' + }, + getBalance: { + desc: 'Get the balance of an address at a given block.', + args: [{ + name: 'addressHexString', + type: 'String', + desc: 'The address to get the balance of' + }, { + name: 'defaultBlock', + type: 'Number|String', + optional: true, + desc: 'If you pass this parameter it will not use the default block set with web3.eth.defaultBlock.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + getStorageAt: { + desc: 'Get the storage at a specific position of an address.', + args: [{ + name: 'addressHexString', + type: 'String', + desc: 'The address to get the storage from.' + }, { + name: 'position', + type: 'Number', + desc: 'The index position of the storage.' + }, { + name: 'defaultBlock', + type: 'Number|String', + optional: true, + desc: 'If you pass this parameter it will not use the default block set with web3.eth.defaultBlock.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + getCode: { + desc: 'Get the code at a specific address.', + args: [{ + name: 'addressHexString', + type: 'String', + desc: 'The address to get the code from.' + }, { + name: 'defaultBlock', + type: 'Number|String', + optional: true, + desc: 'If you pass this parameter it will not use the default block set with web3.eth.defaultBlock.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + getBlock: { + desc: 'Returns a block matching the block number or block hash.', + args: [{ + name: 'blockHashOrBlockNumber', + type: 'String|Number', + desc: 'The block number or hash. Or the string "earliest", "latest" or "pending"' + }, { + name: 'returnTransactionObjects', + type: 'Boolean', + optional: true, + desc: '(default false) If true, the returned block will contain all transactions as objects, if false it will only contains the transaction hashes.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + getBlockTransactionCount: { + desc: 'Returns the number of transaction in a given block.', + args: [{ + name: 'hashStringOrBlockNumber', + type: 'String|Number', + desc: 'The block number or hash. Or the string "earliest", "latest" or "pending"' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + getUncle: { + desc: 'Returns a blocks uncle by a given uncle index position', + args: [{ + name: 'blockHashStringOrNumber', + type: 'String|Number', + desc: 'The block number or hash. Or the string "earliest", "latest" or "pending"' + }, { + name: 'uncleNumber', + type: 'Number', + desc: 'The index position of the uncle.' + }, { + name: 'returnTransactionObjects', + type: 'Boolean', + desc: '(default false) If true, the returned block will contain all transactions as objects, if false it will only contains the transaction hashes.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + getBlockUncleCount: { + desc: '', // TODO missing from docs + }, + getTransaction: { + desc: 'Returns a transaction matching the given transaction hash.', + args: [{ + name: 'transactionHash', + type: 'String', + desc: 'The transaction hash.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + getTransactionFromBlock: { + desc: 'Returns a transaction based on a block hash or number and the transactions index position.', + args: [{ + name: 'hashStringOrBlockNumber', + type: 'String|Number', + desc: 'The block number or hash. Or the string "earliest", "latest" or "pending"' + }, { + name: 'indexNumber', + type: 'Number', + desc: 'The transactions index position.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + getTransactionReceipt: { + desc: 'Returns the receipt of a transaction by transaction hash.', + args: [{ + name: 'hashString', + type: 'String', + desc: 'The transaction hash.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + getTransactionCount: { + desc: 'Get the numbers of transactions sent from this address.', + args: [{ + name: 'addressHexString', + type: 'String', + desc: 'The address to get the numbers of transactions from.' + }, { + name: 'defaultBlock', + type: 'String|Number', + desc: 'If you pass this parameter it will not use the default block set with web3.eth.defaultBlock.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + sendTransaction: { + desc: 'Sends a transaction to the network.', + args: [{ + name: 'transactionObject', + type: 'Object', + desc: 'The transaction object to send: {from[, to][, value][, gas][, gasPrice][, data][, nonce]}' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + sendRawTransaction: { + desc: 'Sends an already signed transaction.', + args: [{ + name: 'signedTransactionData', + type: 'String', + desc: 'Signed transaction data in HEX format' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + sign: { + desc: 'Signs data from a specific account. This account needs to be unlocked.', + args: [{ + name: 'address', + type: 'String', + desc: 'Address to sign with.' + }, { + name: 'dataToSign', + type: 'String', + desc: 'Data to sign.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + call: { + desc: 'Executes a message call transaction, which is directly executed in the VM of the node, but never mined into the blockchain.', + args: [{ + name: 'callObject', + type: 'String', + desc: 'Address to sign with.' + }, { + name: 'defaultBlock', + type: 'String', + optional: true, + desc: 'Data to sign.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + estimateGas: { + desc: 'Executes a message call or transaction, which is directly executed in the VM of the node, but never mined into the blockchain and returns the amount of the gas used.', + args: [{ + name: 'callObject', + type: 'Object', + desc: 'The transaction object to send: {[from][, to][, value][, gas][, gasPrice][, data][, nonce]}' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + filter: { + // TODO: add description + desc: '', + args: [{ + name: 'options', + type: 'String|Object', + desc: 'The string "latest" or "pending" to watch for changes in the latest block or pending transactions respectively. Or a filter options object as follows: {fromBlock: Number|String, toBlock: Number|String, address: String, topics: StringArray}' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'Watch callback' + }] + }, + // TODO filters + // watch : ['callback'], + // stopWatching : ['callback'], + contract: { + desc: 'Creates a contract object for a solidity contract, which can be used to initiate contracts on an address.', + args: [{ + name: 'abiArray', + type: 'Array', + desc: 'ABI array with descriptions of functions and events of the contract.' + }] + }, + getCompilers: { + desc: 'Gets a list of available compilers.', + args: [{ + name: 'callback', + type: 'Function', + optional: true, + desc: 'If you pass a callback the HTTP request is made asynchronous.' + }] + }, + compile: { // TODO we should auto hide these depending on output from getCompilers + lll: { + desc: 'Compiles LLL source code.', + args: [{ + name: 'sourceString', + type: 'String', + desc: 'The LLL source code.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'Watch callback' + }] + }, + solidity: { + desc: 'Compiles solidity source code', + args: [{ + name: 'sourceString', + type: 'String', + desc: 'The solidity source code.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'Watch callback' + }], + }, + serpent: { + desc: 'Compiles serpent source code', + args: [{ + name: 'sourceString', + type: 'String', + desc: 'The serpent source code.' + }, { + name: 'callback', + type: 'Function', + optional: true, + desc: 'Watch callback' + }] + } + }, + namereg: { + desc: 'Returns GlobalRegistrar object.' + } + }, + + db: { + putString: { + desc: 'Store a string in the local leveldb database.', + args: [{ + name: 'db', + type: 'String', + desc: 'The database to store to.' + }, { + name: 'key', + type: 'String', + desc: 'The name of the store.' + }, { + name: 'value', + type: 'String', + desc: 'The string value to store.' + }] + }, + getString: { + desc: 'Retrieve a string from the local leveldb database. (db, key)', + args: [{ + name: 'db', + type: 'String', + desc: 'The database string name to retrieve from.' + }, { + name: 'key', + type: 'String', + desc: 'The name of the store.' + }] + }, + putHex: { + desc: 'Store binary data in the local leveldb database. (db, key, value)', + args: [{ + name: 'db', + type: 'String', + desc: 'The database to store to.' + }, { + name: 'key', + type: 'String', + desc: 'The name of the store.' + }, { + name: 'value', + type: 'String', + desc: 'The HEX string to store.' + }] + }, + getHex: { + desc: 'Retrieve binary data from the local leveldb database. (db, key)', + args: [{ + name: 'db', + type: 'String', + desc: 'The database string name to retrieve from.' + }, { + name: 'key', + type: 'String', + desc: 'The name of the store.' + }] + } + } + } +}; + +function jsSuggestionsContainerStyle(suggestionsCount) { + return { + marginVertical: 1, + marginHorizontal: 0, + //height: Math.min(150, (56 * suggestionsCount)), + backgroundColor: "white", + borderRadius: 5, + keyboardShouldPersistTaps: true + }; +} + +var jsSuggestionContainerStyle = { + paddingLeft: 16, + backgroundColor: "white" +}; + +var jsSubContainerStyle = { + //height: 56, + paddingTop: 9, + borderBottomWidth: 1, + borderBottomColor: "#0000001f" +}; + +var jsValueStyle = { + fontSize: 14, + fontFamily: "font", + color: "#000000de" +}; + +var jsBoldValueStyle = { + fontSize: 14, + fontFamily: "font", + color: "#000000de", + fontWeight: "bold" +}; + +var jsDescriptionStyle = { + marginTop: 1.5, + fontSize: 14, + fontFamily: "font", + color: "#838c93de" +}; + +var messages = []; + + +console = (function(old){ + return { + log: function(text){ + old.log(text); + var message = { + type: 'log', + message: JSON.stringify(text) + }; + messages.push(message); + context.messages.push(message); + }, + info: function (text) { + old.info(text); + context.messages.push({ + type: 'info', + message: JSON.stringify(text) + }); + }, + warn: function (text) { + old.warn(text); + context.messages.push({ + type: 'warn', + message: JSON.stringify(text) + }); + }, + error: function (text) { + old.error(text); + context.messages.push({ + type: 'error', + message: JSON.stringify(text) + }); + } + }; +}(console)); + + + +if (!String.prototype.startsWith) { + String.prototype.startsWith = function(searchString, position){ + position = position || 0; + return this.substr(position, searchString.length) === searchString; + }; +} + +function matchSubString(array, string) { + var matched = []; + for (var i = 0; i < array.length; i++) { + var item = array[i]; + if (item.toLowerCase().startsWith(string.toLowerCase())) { + matched.push(item); + } + } + return matched; +} + +function cleanCode(code) { + // remove comments + var commentsRegex = /\/\*.+?\*\/|\/\/.*/g; + code = code.replace(commentsRegex, ""); + // replace string literals + var literalsRegex = /\"(?:\\\\\"|[^\"])*?\"/g; + code = code.replace(literalsRegex, '""'); + var literalsRegex = /\'(?:\\\\\'|[^\'])*?\'/g; + code = code.replace(literalsRegex, '""'); + + return code +} + +function createObjectSuggestion(name, docInfo, code, parameterNumber) { + var title = name; + if (docInfo.args) { + title += "("; + for (var i = 0; i < docInfo.args.length; i++) { + var argument = docInfo.args[i]; + var argumentText = (i > 0 ? ", " : "") + (parameterNumber === i ? "*" + argument.name + "*" : argument.name); + if (argument.optional) { + argumentText = "[" + argumentText + "]"; + } + title += argumentText; + } + title += ")"; + } + if (!docInfo.desc) { + name += "."; + } else if (docInfo.args) { + name += "("; + if (docInfo.args.length == 0) { + name += ")"; + } + } + var suggestion = { + title: title, + desc: docInfo.desc + }; + + if (code != null) { + suggestion.pressValue = code + name; + } + return suggestion; +} + +var lastMessage = null; + +function getLastForm(code) { + var codeLength = code.length; + var form = ''; + var level = 0; + var index = codeLength - 1; + while(index >= 0) { + var char = code[index]; + if (level == 0 && (char == '(' || char == ',')) { + break; + } + if (char == ')') { + level --; + } + if (char == '(') { + level ++; + } + form = char + form; + index--; + } + return form; +} + +function getLastLevel(code) { + var codeLength = code.length; + var form = ''; + var index = codeLength - 1; + var nested = false; + var level = 0; + while(index >= 0) { + var char = code[index]; + if (char == ')') { + level--; + nested = true; + } + if (char == '(') { + level++; + if (level == 0) { + nested = false; + } + if (level <= 0) { + form = "argument" + form; + index--; + continue; + } + } + if ((level == 1 && char == ',') || level == 2) { + break; + } + if (!nested || level > 0) { + form = char + form; + } + index--; + } + if (form.indexOf("(") < 0) { + var parts = form.split(','); + form = parts[parts.length - 1]; + } + return form; +} + +function getPartialSuggestions(doc, fullCode, code) { + var suggestions = []; + var functionParts = code.split("("); + var objectParts = code.split(/[^a-zA-Z_0-9\$\-\u00C0-\u1FFF\u2C00-\uD7FF\w]+/); + + var index = 0; + var suggestedFunction = ''; + while (index < objectParts.length) { + var part = objectParts[index]; + if (part != "desc" && part != "args" && doc[part] != null) { + doc = doc[part]; + suggestedFunction += part + '.'; + index++; + } else { + break; + } + } + suggestedFunction = suggestedFunction.substring(0, suggestedFunction.length - 1) + if (functionParts.length == 1) { + // object suggestions + if (index > objectParts.length - 1) { + var suggestion = objectParts[objectParts.length - 1]; + suggestions.push(createObjectSuggestion(suggestion, doc, fullCode.substring(0, fullCode.length - suggestion.length))); + } else if (index === objectParts.length - 1) { + var lastPart = objectParts[index]; + var keys = Object.keys(doc); + var matches = matchSubString(keys, lastPart); + + for (var i = 0; i < matches.length; i++) { + var suggestion = matches[i]; + if (suggestion == "desc" || suggestion == "args") { + continue; + } + var docInfo = doc[suggestion]; + if (docInfo != null) { + suggestions.push(createObjectSuggestion(suggestion, docInfo, fullCode.substring(0, fullCode.length - lastPart.length))); + } + } + } + } else if (functionParts.length == 2) { + // parameter suggestions + var parameters = functionParts[1].split(","); + if (doc.args && parameters.length <= doc.args.length && parameters[parameters.length - 1].indexOf(")") < 0) { + var paramInfo = doc.args[parameters.length - 1]; + var docInfo = doc; + docInfo.desc = paramInfo.name + ": " + paramInfo.desc; + suggestions.push(createObjectSuggestion(suggestedFunction, docInfo, null, parameters.length - 1)); + } + } + console.log(suggestions); + return suggestions; +} + +function getJsSuggestions(code, context) { + var suggestions = []; + var doc = DOC_MAP; + // TODO: what's /c / doing there ??? + console.log(code); + if (!code || code == "" || code == "c ") { + code = ""; + console.log("Last message: " + context.data); + if (context.data != null) { + suggestions.push({ + title: 'Last command used:', + desc: context.data, + pressValue: context.data + }); + } + var keys = Object.keys(doc); + for (var i = 0; i < keys.length; i++) { + var suggestion = keys[i]; + var docInfo = doc[suggestion]; + if (docInfo != null) { + suggestions.push(createObjectSuggestion(suggestion, docInfo, "")); + } + } + } else { + // TODO: what's /c / doing there ??? + if (code.startsWith("c ")) { + code = code.substring(2); + } + var originalCode = code; + code = cleanCode(code); + var levelCode = getLastLevel(code); + var code = getLastForm(levelCode); + if (levelCode != code) { + suggestions = getPartialSuggestions(doc, originalCode, levelCode); + } + + console.log("Final code: " + code); + console.log("Level code: " + levelCode); + suggestions = suggestions.concat(getPartialSuggestions(doc, originalCode, code)); + } + console.log(suggestions); + return suggestions; +} + +function createMarkupText(text) { + var parts = []; + var index = 0; + var currentText = ''; + var isBold = false; + while (index < text.length) { + var char = text[index]; + if (char == '*') { + if (currentText != '') { + parts.push( + status.components.text( + {style: isBold ? jsBoldValueStyle : jsValueStyle}, + currentText + ) + ); + currentText = ''; + } + isBold = !isBold; + } else { + currentText += char; + } + index++; + } + if (currentText != '') { + parts.push( + status.components.text( + {style: isBold ? jsBoldValueStyle : jsValueStyle}, + currentText + ) + ); + } + console.log(parts); + return parts; +} + +function jsSuggestions(params, context) { + + console.log(context); + + var suggestions = getJsSuggestions(params.code, context); + var sugestionsMarkup = []; + + for (var i = 0; i < suggestions.length; i++) { + var suggestion = suggestions[i]; + + if (suggestion.title.indexOf('*') >= 0) { + suggestion.title = createMarkupText(suggestion.title); + } + var suggestionMarkup = status.components.view(jsSuggestionContainerStyle, + [status.components.view(jsSubContainerStyle, + [ + status.components.text({style: jsValueStyle}, + suggestion.title), + status.components.text({style: jsDescriptionStyle}, + suggestion.desc) + ])]); + if (suggestion.pressValue) { + suggestionMarkup = status.components.touchable({ + onPress: [status.events.SET_VALUE, suggestion.pressValue]}, + suggestionMarkup); + } + sugestionsMarkup.push(suggestionMarkup); + } + + if (sugestionsMarkup.length > 0) { + var view = status.components.scrollView(jsSuggestionsContainerStyle(sugestionsMarkup.length), + sugestionsMarkup + ); + return {markup: view}; + } +} + + + +function jsHandler(params, context) { + var result = { + err: null, + data: null, + messages: [] + }; + messages = []; + try { + result.data = JSON.stringify(eval(params.code)); + localStorage.set(params.code); + } catch(e) { + result.err = e; + } + + result.messages = messages; + + return result; +} + +status.command({ + name: "js", + title: "Javascript", + description: "Evaluate Javascript", + color: "#7099e6", + params: [{ + name: "code", + type: status.types.TEXT, + suggestions: jsSuggestions, + placeholder: "Code" + }], + preview: function (params) { + return status.components.text( + {}, + params.code + ); + }, + validator: function (params) { + var error = null; + lastMessage = params.code; + try { + eval(params.code); + } catch(e) { + error = e; + } + if (error) { + var error = status.components.validationMessage( + "Invalid js", + error + ); + + return {errors: [error]}; + } + }, + handler: jsHandler +}); + + var phones = [ // TODO this is supposed to be regionalised { number: "89171111111", @@ -782,3 +1807,4 @@ status.response({ return status.components.text({style: style}, "●●●●●●●●●●"); } }); + diff --git a/resources/status.js b/resources/status.js index 66d7f17779..e25ae39025 100644 --- a/resources/status.js +++ b/resources/status.js @@ -80,17 +80,21 @@ function call(pathStr, paramsStr) { return null; } + context.messages = []; + callResult = fn(params.parameters, params.context); result = { returned: callResult, - context: context[message_id] + context: context[message_id], + messages: context.messages }; return JSON.stringify(result); } function text(options, s) { - return ['text', options, s]; + s = Array.isArray(s) ? s : [s]; + return ['text', options].concat(s); } function view(options, elements) { diff --git a/src/status_im/chat/handlers/commands.cljs b/src/status_im/chat/handlers/commands.cljs index c8099f7b92..acd80a556e 100644 --- a/src/status_im/chat/handlers/commands.cljs +++ b/src/status_im/chat/handlers/commands.cljs @@ -3,6 +3,7 @@ [status-im.utils.handlers :refer [register-handler] :as u] [status-im.components.status :as status] [status-im.models.commands :as commands] + [status-im.chat.utils :refer [console? not-console?]] [clojure.string :as str] [status-im.commands.utils :as cu] [status-im.utils.phone-number :as pn] @@ -21,13 +22,15 @@ [{:keys [current-chat-id canceled-command] :as db} _] (when-not canceled-command (let [{:keys [command content params]} (get-in db [:chats current-chat-id :command-input]) + data (get-in db [:local-storage current-chat-id]) {:keys [name type]} command path [(if (= :command type) :commands :responses) name :params 0 :suggestions] - params {:parameters (or params {})}] + params {:parameters (or params {}) + :context {:data data}}] (status/call-jail current-chat-id path params @@ -62,6 +65,16 @@ (assoc-in db [:chats current-chat-id :input-text] nil) (assoc db :canceled-command (and command? (not starts-as-command?))))))) +(register-handler :fill-chat-command-content + (u/side-effect! + (fn [db [_ content]] + (let [command? (= :command (current-command db :type))] + (dispatch + [:set-chat-command-content + (if command? + (str cu/command-prefix content) + content)]))))) + (defn invoke-command-preview! [{:keys [staged-command] :as db} [_ command-input chat-id]] (let [{:keys [command id]} staged-command @@ -72,10 +85,12 @@ :preview] params {:parameters parameters :context {:platform platform/platform}}] - (status/call-jail chat-id - path - params - #(dispatch [:command-preview chat-id id %])))) + (if (and (console? chat-id) (= name "js")) + (dispatch [:send-chat-message]) + (status/call-jail chat-id + path + params + #(dispatch [:command-preview chat-id id %]))))) (defn command-input ([{:keys [current-chat-id] :as db}] @@ -270,3 +285,4 @@ (if (= :on-send suggestions-trigger) (dispatch [:invoke-commands-suggestions!]) (dispatch [:stage-command])))))) + diff --git a/src/status_im/chat/handlers/console.cljs b/src/status_im/chat/handlers/console.cljs index 8bdccf0157..270a4912ac 100644 --- a/src/status_im/chat/handlers/console.cljs +++ b/src/status_im/chat/handlers/console.cljs @@ -1,8 +1,11 @@ (ns status-im.chat.handlers.console (:require [re-frame.core :refer [dispatch dispatch-sync after]] [status-im.utils.handlers :refer [register-handler] :as u] - [status-im.constants :refer [console-chat-id]] - [status-im.data-store.messages :as messages])) + [status-im.constants :refer [console-chat-id + text-content-type]] + [status-im.data-store.messages :as messages] + [taoensso.timbre :as log] + [status-im.utils.random :as random])) (def console-commands {:password @@ -37,3 +40,37 @@ :message-status status}))) (fn [db [_ message-id status]] (assoc-in db [:message-statuses message-id] {:status status}))) + +(register-handler :console-respond-command + (u/side-effect! + (fn [_ [_ {:keys [command] :as parameters}]] + (let [{:keys [command handler-data]} command] + (when command + (let [{:keys [name]} command] + (case name + "js" (let [{:keys [err data messages]} handler-data + content (if err + err + data)] + (doseq [message messages] + (let [{:keys [message type]} message] + (dispatch [:received-message + {:message-id (random/id) + :content (str type ": " message) + :content-type text-content-type + :outgoing false + :chat-id console-chat-id + :from console-chat-id + :to "me"}]))) + (when content + (dispatch [:received-message + {:message-id (random/id) + :content (str content) + :content-type text-content-type + :outgoing false + :chat-id console-chat-id + :from console-chat-id + :to "me"}]))) + (log/debug "ignoring command: " command)))))))) + + diff --git a/src/status_im/chat/handlers/send_message.cljs b/src/status_im/chat/handlers/send_message.cljs index 478ecc2b62..1002c8be52 100644 --- a/src/status_im/chat/handlers/send_message.cljs +++ b/src/status_im/chat/handlers/send_message.cljs @@ -103,7 +103,8 @@ (log/debug "Handler data: " request handler-data (dissoc params :commands :staged-command)) (dispatch [:clear-command chat-id (:id staged-command)]) (dispatch [::send-command! add-to-chat-id (assoc params :command command')]) - + (when (cu/console? chat-id) + (dispatch [:console-respond-command params])) (when (and (= "send" (get-in staged-command [:command :name])) (not= add-to-chat-id wallet-chat-id)) (let [ct (if request diff --git a/src/status_im/chat/views/new_message.cljs b/src/status_im/chat/views/new_message.cljs index 02a8844fa4..f526bb7cd6 100644 --- a/src/status_im/chat/views/new_message.cljs +++ b/src/status_im/chat/views/new_message.cljs @@ -34,13 +34,17 @@ parameter [:get-command-parameter] type [:command-type] suggestions [:get-suggestions] - staged-commands [:get-chat-staged-commands]] + staged-commands [:get-chat-staged-commands] + message-input-height [:get-message-input-view-height]] (let [on-top? (or (and (not (empty? suggestions)) (not command?)) (not= response-height input-height)) style (when-not (seq staged-commands) (get-in platform-specific [:component-styles :chat :new-message]))] [view {:style (merge (st/new-message-container margin on-top?) style) - :on-layout #(dispatch [:set-message-input-view-height (get-height %)])} + :on-layout (fn [event] + (let [height (get-height event)] + (when (not= height message-input-height) + (dispatch [:set-message-input-view-height height]))))} [plain-message-input-view (when command? (get-options parameter type))]])) diff --git a/src/status_im/commands/handlers/jail.cljs b/src/status_im/commands/handlers/jail.cljs index e29a4f76f3..84a14d2a43 100644 --- a/src/status_im/commands/handlers/jail.cljs +++ b/src/status_im/commands/handlers/jail.cljs @@ -52,7 +52,7 @@ (defn suggestions-events-handler! [db [[n data]]] (case (keyword n) - :set-value (dispatch [:set-chat-command-content data]) + :set-value (dispatch [:fill-chat-command-content data]) ;; todo show error? nil)) @@ -92,3 +92,8 @@ (reg-handler :command-preview (after (print-error-message! "Error on command preview")) command-preview) + +(reg-handler :set-local-storage + (fn [{:keys [current-chat-id] :as db} [{:keys [data] :as event}]] + (log/debug "Got event: " event) + (assoc-in db [:local-storage current-chat-id] data))) \ No newline at end of file diff --git a/src/status_im/components/status.cljs b/src/status_im/components/status.cljs index 31253ad127..2713372d94 100644 --- a/src/status_im/components/status.cljs +++ b/src/status_im/components/status.cljs @@ -26,7 +26,7 @@ (swap! calls conj args)) (defn call-module [f] - (log/debug :call-module f) + ;(log/debug :call-module f) (if @module-initialized? (f) (store-call f))) @@ -106,11 +106,15 @@ :debug js/goog.DEBUG :locale i/i18n.locale) cb (fn [r] - (let [r' (t/json->clj r)] + (let [{:keys [result] :as r'} (t/json->clj r) + {:keys [messages]} result] (log/debug r') + (doseq [{:keys [type message]} messages] + (log/debug (str "VM console(" type ") - " message))) (callback r')))] (.callJail status chat-id (cljs->json path) (cljs->json params') cb)))))) + (defn set-soft-input-mode [mode] (when status (call-module #(.setSoftInputMode status mode)))) diff --git a/src/status_im/handlers.cljs b/src/status_im/handlers.cljs index 305157b056..fe8c494b20 100644 --- a/src/status_im/handlers.cljs +++ b/src/status_im/handlers.cljs @@ -121,6 +121,7 @@ "transaction.failed" (dispatch [:transaction-failed event]) "node.started" (log/debug "Event *node.started* received") "module.initialized" (dispatch [:status-module-initialized!]) + "local_storage.set" (dispatch [:set-local-storage event]) (log/debug "Event " type " not handled")))))) (register-handler :status-module-initialized!