From 5c15df9b646421329e9b65299841eebce49024ba Mon Sep 17 00:00:00 2001 From: alwx Date: Wed, 10 May 2017 19:42:52 +0300 Subject: [PATCH] /send and /request commands; commands in group chats and fixes for parameter and result boxes --- android/app/build.gradle | 1 + .../java/im/status/ethereum/MainActivity.java | 2 + .../main/res/drawable-hdpi/icon_back_gray.png | Bin 0 -> 555 bytes .../main/res/drawable-mdpi/icon_back_gray.png | Bin 0 -> 372 bytes .../res/drawable-xhdpi/icon_back_gray.png | Bin 0 -> 555 bytes .../res/drawable-xxhdpi/icon_back_gray.png | Bin 0 -> 747 bytes bots/console/bot.js | 4 +- bots/wallet/bot.js | 559 +++++++++++++++--- bots/wallet/translations.js | 17 +- externs/externs.js | 30 +- .../icon_back_gray.imageset/Contents.json | 23 + .../icon_back_gray.imageset/iconBackDark.png | Bin 0 -> 372 bytes .../iconBackDark@2x.png | Bin 0 -> 555 bytes .../iconBackDark@3x.png | Bin 0 -> 747 bytes resources/status.js | 16 +- src/status_im/bots/handlers.cljs | 47 +- src/status_im/chat/handlers.cljs | 2 +- src/status_im/chat/handlers/commands.cljs | 33 +- src/status_im/chat/handlers/input.cljs | 118 +++- .../chat/handlers/receive_message.cljs | 2 +- src/status_im/chat/handlers/send_message.cljs | 19 +- src/status_im/chat/models/input.cljs | 74 ++- src/status_im/chat/models/suggestions.cljs | 14 +- .../chat/styles/input/box_header.cljs | 41 ++ .../chat/styles/input/result_box.cljs | 28 - src/status_im/chat/subs.cljs | 25 +- .../chat/views/choosers/choose_contact.cljs | 47 ++ .../chat/views/input/box_header.cljs | 39 ++ src/status_im/chat/views/input/input.cljs | 18 +- .../chat/views/input/parameter_box.cljs | 26 +- .../chat/views/input/result_box.cljs | 21 +- src/status_im/chat/views/input/web_view.cljs | 5 +- src/status_im/chat/views/message/message.cljs | 22 +- .../chat/views/message/request_message.cljs | 17 +- src/status_im/commands/handlers/jail.cljs | 37 +- src/status_im/commands/handlers/loading.cljs | 83 +-- src/status_im/commands/utils.cljs | 7 +- src/status_im/contacts/handlers.cljs | 2 +- src/status_im/contacts/subs.cljs | 5 + src/status_im/data_store/contacts.cljs | 11 +- src/status_im/data_store/messages.cljs | 23 +- .../realm/schemas/account/core.cljs | 8 +- .../realm/schemas/account/v10/core.cljs | 39 ++ .../realm/schemas/account/v10/message.cljs | 51 ++ .../realm/schemas/account/v8/core.cljs | 3 +- src/status_im/utils/js_resources.cljs | 2 - 46 files changed, 1143 insertions(+), 378 deletions(-) create mode 100644 android/app/src/main/res/drawable-hdpi/icon_back_gray.png create mode 100644 android/app/src/main/res/drawable-mdpi/icon_back_gray.png create mode 100644 android/app/src/main/res/drawable-xhdpi/icon_back_gray.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/icon_back_gray.png create mode 100644 ios/StatusIm/Images.xcassets/icon_back_gray.imageset/Contents.json create mode 100644 ios/StatusIm/Images.xcassets/icon_back_gray.imageset/iconBackDark.png create mode 100644 ios/StatusIm/Images.xcassets/icon_back_gray.imageset/iconBackDark@2x.png create mode 100644 ios/StatusIm/Images.xcassets/icon_back_gray.imageset/iconBackDark@3x.png create mode 100644 src/status_im/chat/styles/input/box_header.cljs create mode 100644 src/status_im/chat/views/choosers/choose_contact.cljs create mode 100644 src/status_im/chat/views/input/box_header.cljs create mode 100644 src/status_im/data_store/realm/schemas/account/v10/core.cljs create mode 100644 src/status_im/data_store/realm/schemas/account/v10/message.cljs diff --git a/android/app/build.gradle b/android/app/build.gradle index 53f590ef23..1d0e8daf83 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -180,6 +180,7 @@ dependencies { compile project(':react-native-fs') compile project(':react-native-image-crop-picker') compile project(':react-native-webview-bridge') + compile 'testfairy:testfairy-android-sdk:1.+@aar' compile fileTree(dir: "node_modules/realm/android/libs", include: ["*.jar"]) } diff --git a/android/app/src/main/java/im/status/ethereum/MainActivity.java b/android/app/src/main/java/im/status/ethereum/MainActivity.java index 95f124e981..97ef701856 100644 --- a/android/app/src/main/java/im/status/ethereum/MainActivity.java +++ b/android/app/src/main/java/im/status/ethereum/MainActivity.java @@ -9,6 +9,7 @@ import android.content.res.Configuration; import android.os.Bundle; import com.facebook.react.ReactActivity; import com.cboy.rn.splashscreen.SplashScreen; +import com.testfairy.TestFairy; import java.util.Properties; @@ -26,6 +27,7 @@ public class MainActivity extends ReactActivity { protected void onCreate(Bundle savedInstanceState) { SplashScreen.show(this); super.onCreate(savedInstanceState); + TestFairy.begin(this, "33e4bd97daaaacf3b1b6425096fb65186248fe44"); if (!RootUtil.isDeviceRooted()) { configureStatus(); diff --git a/android/app/src/main/res/drawable-hdpi/icon_back_gray.png b/android/app/src/main/res/drawable-hdpi/icon_back_gray.png new file mode 100644 index 0000000000000000000000000000000000000000..7f3b34c5aa67d565164d807e142e1c8f04838f5d GIT binary patch literal 555 zcmV+`0@VG9P)Px$e5m@!MkKorOC(n7_!sNkgd0sIu*)Q(Clv{(gqKZWkDF16SU78ju@ zZv84QPJ#{5kJ zw6BBP(=mz69PKc=_?b-KNai`g4G zuK$8>HEVqxYZw!tfX4ug2vETr5uk+EEhRlo+kHUV4kS_Evu7Z$J$Ur3?=qMzwAeyJ?radr)< zPZL5RSV^#12(Q*z(noPr7HBnUFPwP|+Y*a+ST!w79FxZTy!o`htm~E4T>K1bexCC4RE`huRegXUwHQG}^6gvO_002ovPDHLkV1h*+=|}(o literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-mdpi/icon_back_gray.png b/android/app/src/main/res/drawable-mdpi/icon_back_gray.png new file mode 100644 index 0000000000000000000000000000000000000000..3782cb217cf16cbb9a2b5564209d216198bb3b0b GIT binary patch literal 372 zcmV-)0gL{LP)Px$E=fc|R7ef&mAz`iFc5(6oIm2u3FHYkyZx6rf41FD zf=l5nw+iaOt%AC678qZVd@UbJL!D~QP<0puK#=6g)ao8mbFNBN=EhBphh~TfUqtYf zq?w4KxToxOnp&H@c>nBY$$rBUmicXO6o#)qPIQ@M6)>od#7V#sE)SQf>P?&lmr9IH zW~e&2_MYDwpBw*AvAV9CR}wH_8oYdSqY-cm`RBSaLw68uO#Tb52iF>JGI#z@^0000Px$e5m@!MkKorOC(n7_!sNkgd0sIu*)Q(Clv{(gqKZWkDF16SU78ju@ zZv84QPJ#{5kJ zw6BBP(=mz69PKc=_?b-KNai`g4G zuK$8>HEVqxYZw!tfX4ug2vETr5uk+EEhRlo+kHUV4kS_Evu7Z$J$Ur3?=qMzwAeyJ?radr)< zPZL5RSV^#12(Q*z(noPr7HBnUFPwP|+Y*a+ST!w79FxZTy!o`htm~E4T>K1bexCC4RE`huRegXUwHQG}^6gvO_002ovPDHLkV1h*+=|}(o literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_back_gray.png b/android/app/src/main/res/drawable-xxhdpi/icon_back_gray.png new file mode 100644 index 0000000000000000000000000000000000000000..0bcd6a9b088459b560d3264fd49ae102c2e92bba GIT binary patch literal 747 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!jKx9jP7LeL$-HD>U|Qgy{m#cain6c1_uUa!M z>Cw&I_6PnkbV>wFT%=-sF4(PW_SB{4X4mTPu}zt*`1{_T{B-jVW?m=Nn3NPy&^^{I z-#bn))^Dqp&(fM5y!m2acIs0u%ehI+Teg?~s4A%3x4P#ii$>JT$t%RxZoM|UeTIi% zT0#8A_XT?AcJr}6l1hE%9(m0#IQa9_^ca~>4*V9Ube?k=q&>{PRykk#yM=?)3(q;# zEo;oKsLRg?!ylf+n*zH5hx zEf4IE_FUx7T3RWs#_Gb$KIJ4s;1NY;7twYWfz&yt8dp3H;X1;&+(<#sVk$%7Ax^#{ zyy_YP?sF0xVhWoW3XeImKk94dGU2qyaM)k9_t*-1n+PhxUCeQlY`19_|k~p#2_DdFA7efBt-FB^N|B3)V4-E&;O(mbp-0J7Cs9c$6 z66022d9X_}%C~`IMxl)Oz3p9nyoP;TEUX3YPCyrmuyhy)87QptaBQ?tVr75S7Q!i@ zzT{NH4uw#l?^YHmyz)?K&`_SpxTwp8t3yf1QX$JjK{rMBk zVx5zsPt@X;g$kL*oy}cWXH$Iq$Gyohj_XbsX?xy{Ilt?D@*7WP$AkV#nWcAr3H?9V zP-u9P`ER#K=GMn*dH)a8tGZO=*RrI)r?Z=PKyo;!c`(UVh)G15yiOCICL X@^259^PO1&OuY=Au6{1-oD!M<&}%ss literal 0 HcmV?d00001 diff --git a/bots/console/bot.js b/bots/console/bot.js index 8854757ffc..26d560bb3c 100644 --- a/bots/console/bot.js +++ b/bots/console/bot.js @@ -486,7 +486,7 @@ var faucets = [ function faucetSuggestions(params) { var suggestions = faucets.map(function (entry) { return status.components.touchable( - {onPress: status.components.dispatch([status.events.SET_COMMAND_ARGUMENT, [0, entry.url]])}, + {onPress: status.components.dispatch([status.events.SET_COMMAND_ARGUMENT, [0, entry.url, false]])}, status.components.view( suggestionContainerStyle, [status.components.view( @@ -561,7 +561,7 @@ status.command({ function debugSuggestions(params) { var suggestions = ["On", "Off"].map(function (entry) { return status.components.touchable( - {onPress: status.components.dispatch([status.events.SET_COMMAND_ARGUMENT, [0, entry]])}, + {onPress: status.components.dispatch([status.events.SET_COMMAND_ARGUMENT, [0, entry, false]])}, status.components.view( suggestionContainerStyle, [status.components.view( diff --git a/bots/wallet/bot.js b/bots/wallet/bot.js index 1e0b11b5ea..27ce9de060 100644 --- a/bots/wallet/bot.js +++ b/bots/wallet/bot.js @@ -1,5 +1,271 @@ +function calculateFee(n, tx) { + var estimatedGas = 21000; + if (tx !== null) { + estimatedGas = web3.eth.estimateGas(tx); + } + + var gasMultiplicator = Math.pow(1.4, n).toFixed(3); + return web3.fromWei(web3.eth.gasPrice * gasMultiplicator * estimatedGas, "ether"); +} + +function calculateGasPrice(n) { + var gasMultiplicator = Math.pow(1.4, n).toFixed(3); + return web3.eth.gasPrice * gasMultiplicator; +} + +status.defineSubscription( + "calculatedFee", + {value: ["sliderValue"], tx: ["transaction"]}, + function (params) { + return calculateFee(params.value, params.tx); + } +); + +function getFeeExplanation(n) { + return I18n.t('send_explanation') + I18n.t('send_explanation_' + (n + 2)); +} + +status.defineSubscription( + "feeExplanation", + {value: ["sliderValue"]}, + function(params) { + return getFeeExplanation(params.value); + } +) + +function amountParameterBox(params, context) { + if (!params["bot-db"]) { + params["bot-db"] = {}; + } + + var contactAddress; + if (params["bot-db"]["public"] && params["bot-db"]["public"]["recipient"]) { + contactAddress = params["bot-db"]["public"]["recipient"]["address"]; + } else { + contactAddress = null; + } + + var txData; + var amount; + try { + amount = params.args[1]; + txData = { + to: contactAddress, + value: web3.toWei(amount) || 0 + }; + } catch (err) { + amount = null; + txData = { + to: contactAddress, + value: 0 + }; + } + + var sliderValue = params["bot-db"]["sliderValue"] || 0; + + status.setDefaultDb({ + transaction: txData, + calculatedFee: calculateFee(sliderValue, txData), + feeExplanation: getFeeExplanation(sliderValue), + sliderValue: sliderValue + }); + + return { + title: I18n.t('send_title'), + showBack: true, + markup: status.components.scrollView( + { + keyboardShouldPersistTaps: "always" + }, + [status.components.view( + { + flex: 1 + }, + [ + status.components.text( + { + style: { + fontSize: 14, + color: "rgb(147, 155, 161)", + paddingTop: 12, + paddingLeft: 16, + paddingRight: 16, + paddingBottom: 20 + } + }, + I18n.t('send_specify_amount') + ), + status.components.touchable( + { + onPress: status.components.dispatch([status.events.FOCUS_INPUT, []]) + }, + status.components.view( + { + flexDirection: "row", + alignItems: "center", + textAlign: "center", + justifyContent: "center" + }, + [ + status.components.text( + { + font: "light", + numberOfLines: 1, + ellipsizeMode: "tail", + style: { + maxWidth: 250, + fontSize: 38, + marginLeft: 8, + color: "black" + } + }, + amount || "0.00" + ), + status.components.text( + { + font: "light", + style: { + fontSize: 38, + marginLeft: 8, + color: "rgb(147, 155, 161)" + } + }, + I18n.t('eth') + ), + ] + ) + ), + status.components.text( + { + style: { + fontSize: 14, + color: "rgb(147, 155, 161)", + paddingTop: 14, + paddingLeft: 16, + paddingRight: 16, + paddingBottom: 5 + } + }, + I18n.t('send_fee') + ), + status.components.view( + { + flexDirection: "row" + }, + [ + status.components.text( + { + style: { + fontSize: 17, + color: "black", + paddingLeft: 16 + } + }, + [status.components.subscribe(["calculatedFee"])] + ), + status.components.text( + { + style: { + fontSize: 17, + color: "rgb(147, 155, 161)", + paddingLeft: 4, + paddingRight: 4 + } + }, + I18n.t('eth') + ) + ] + ), + status.components.slider( + { + maximumValue: 2, + minimumValue: -2, + onSlidingComplete: status.components.dispatch( + [status.events.UPDATE_DB, "sliderValue"] + ), + step: 1, + style: { + marginLeft: 16, + marginRight: 16 + } + } + ), + status.components.view( + { + flexDirection: "row" + }, + [ + status.components.text( + { + style: { + flex: 1, + fontSize: 14, + color: "rgb(147, 155, 161)", + paddingLeft: 16, + alignSelf: "flex-start" + } + }, + I18n.t('send_cheaper') + ), + status.components.text( + { + style: { + flex: 1, + fontSize: 14, + color: "rgb(147, 155, 161)", + paddingRight: 16, + alignSelf: "flex-end", + textAlign: "right" + } + }, + I18n.t('send_faster') + ) + ] + ), + status.components.text( + { + style: { + fontSize: 14, + color: "black", + paddingTop: 16, + paddingLeft: 16, + paddingRight: 16, + paddingBottom: 16, + lineHeight: 24 + } + }, + [status.components.subscribe(["feeExplanation"])] + ) + ] + )] + ) + }; +} + +var paramsSend = [ + { + name: "recipient", + type: status.types.TEXT, + suggestions: function (params) { + return { + title: I18n.t('send_title'), + markup: status.components.chooseContact(I18n.t('send_choose_recipient'), "recipient", 0) + }; + } + }, + { + name: "amount", + type: status.types.NUMBER, + suggestions: amountParameterBox + } +]; + function validateSend(params, context) { - if (!context.to) { + if (!params["bot-db"]) { + params["bot-db"] = {}; + } + + if (!params["bot-db"]["public"] || !params["bot-db"]["public"]["recipient"] || !params["bot-db"]["public"]["recipient"]["address"]) { return { markup: status.components.validationMessage( "Wrong address", @@ -7,7 +273,8 @@ function validateSend(params, context) { ) }; } - if (!params.amount) { + + if (!params["amount"]) { return { markup: status.components.validationMessage( I18n.t('validation_title'), @@ -16,7 +283,7 @@ function validateSend(params, context) { }; } - var amount = params.amount.replace(",", "."); + var amount = params["amount"].replace(",", "."); var amountSplitted = amount.split("."); if (amountSplitted.length === 2 && amountSplitted[1].length > 18) { return { @@ -27,6 +294,15 @@ function validateSend(params, context) { }; } + if (isNaN(parseFloat(params.amount.replace(",", ".")))) { + return { + markup: status.components.validationMessage( + I18n.t('validation_title'), + I18n.t('validation_invalid_number') + ) + }; + } + try { var val = web3.toWei(amount, "ether"); if (val <= 0) { @@ -42,13 +318,15 @@ function validateSend(params, context) { } var balance = web3.eth.getBalance(context.from); - var estimatedGas = web3.eth.estimateGas({ - from: context.from, - to: context.to, - value: val - }); + var fee = calculateFee( + params["bot-db"]["sliderValue"], + { + to: params["bot-db"]["public"]["recipient"]["address"], + value: val + } + ); - if (bn(val).plus(bn(estimatedGas)).greaterThan(bn(balance))) { + if (bn(val).plus(bn(web3.toWei(fee, "ether"))).greaterThan(bn(balance))) { return { markup: status.components.validationMessage( I18n.t('validation_title'), @@ -60,11 +338,14 @@ function validateSend(params, context) { } } -function sendTransaction(params, context) { +function handleSend(params, context) { + var val = web3.toWei(params["amount"].replace(",", "."), "ether"); + var data = { from: context.from, - to: context.to, - value: web3.toWei(params.amount.replace(",", "."), "ether") + to: params["bot-db"]["public"]["recipient"]["address"], + value: val, + gasPrice: calculateGasPrice(params["bot-db"]["sliderValue"]) }; try { @@ -74,107 +355,158 @@ function sendTransaction(params, context) { } } +function previewSend(params, context) { + var amountStyle = { + fontSize: 36, + color: "#000000", + height: 40 + }; + + var amount = status.components.view( + { + flexDirection: "column", + alignItems: "flex-end", + maxWidth: 250 + }, + [status.components.text( + { + style: amountStyle, + numberOfLines: 1, + ellipsizeMode: "tail", + font: "light" + }, + status.localizeNumber(params.amount, context.delimiter, context.separator) + )]); + + var currency = status.components.view( + { + style: { + flexDirection: "column", + justifyContent: "flex-end", + paddingBottom: 0 + } + }, + [status.components.text( + { + style: { + color: "#9199a0", + fontSize: 16, + lineHeight: 18, + marginLeft: 7.5 + } + }, + I18n.t('eth') + )] + ); + + var firstRow = status.components.view( + { + style: { + flexDirection: "row", + justifyContent: "space-between", + marginTop: 8, + marginBottom: 8 + } + }, + [amount, currency] + ); + + var markup; + if (params["bot-db"] + && params["bot-db"]["public"] + && params["bot-db"]["public"]["recipient"] + && context["chat"]["group-chat"] === true) { + var secondRow = status.components.text( + { + style: { + color: "#9199a0", + fontSize: 14, + lineHeight: 18 + } + }, + "to " + params["bot-db"]["public"]["recipient"]["name"] + ); + markup = [firstRow, secondRow]; + } else { + markup = [firstRow]; + } + + return { + markup: status.components.view( + { + style: { + flexDirection: "column" + } + }, + markup + ) + }; +} + +function shortPreviewSend(params, context) { + return { + markup: status.components.text( + {}, + I18n.t('send_title') + ": " + + status.localizeNumber(params.amount, context.delimiter, context.separator) + + " ETH" + ) + }; +} + var send = { name: "send", icon: "money_white", color: "#5fc48d", title: I18n.t('send_title'), description: I18n.t('send_description'), - sequentialParams: true, - params: [{ - name: "amount", - type: status.types.NUMBER - }], - preview: function (params, context) { - var amountStyle = { - fontSize: 36, - color: "#000000", - height: 40 - }; - - var amount = status.components.view( - { - flexDirection: "column", - alignItems: "flex-end", - }, - [status.components.text( - { - style: amountStyle, - font: "light" - }, - status.localizeNumber(params.amount, context.delimiter, context.separator) - )]); - - var currency = status.components.view( - { - style: { - flexDirection: "column", - justifyContent: "flex-end", - paddingBottom: 0 - } - }, - [status.components.text( - { - style: { - color: "#9199a0", - fontSize: 16, - lineHeight: 18, - marginLeft: 7.5 - } - }, - "ETH" - )] - ); - - return { - markup: status.components.view( - { - style: { - flexDirection: "row", - justifyContent: "space-between", - marginTop: 8, - marginBottom: 8 - } - }, - [amount, currency] - ) - }; - }, - shortPreview: function (params, context) { - return { - markup: status.components.text( - {}, - I18n.t('send_title') + ": " - + status.localizeNumber(params.amount, context.delimiter, context.separator) - + " ETH" - ) - }; - }, - handler: sendTransaction, - validator: validateSend + params: paramsSend, + validator: validateSend, + handler: handleSend, + preview: previewSend, + shortPreview: shortPreviewSend }; status.command(send); status.response(send); +var paramsRequest = [ + { + name: "recipient", + type: status.types.TEXT, + suggestions: function (params) { + return { + title: I18n.t('request_title'), + markup: status.components.chooseContact(I18n.t('send_choose_recipient'), "recipient", 0) + }; + } + }, + { + name: "amount", + type: status.types.NUMBER + } +]; + status.command({ name: "request", color: "#5fc48d", title: I18n.t('request_title'), description: I18n.t('request_description'), - sequentialParams: true, - params: [{ - name: "amount", - type: status.types.NUMBER - }], - handler: function (params) { + params: paramsRequest, + handler: function (params, context) { + var val = params["amount"].replace(",", "."); + return { event: "request", - params: [params.amount], request: { command: "send", params: { - amount: params.amount + recipient: context["current-account"]["name"], + amount: val + }, + prefill: [context["current-account"]["name"], val], + prefillBotDb: { + contact: context["current-account"] } } }; @@ -200,8 +532,49 @@ status.command({ }; }, validator: function (params) { + if (!params["bot-db"]) { + params["bot-db"] = {}; + } + + if (!params["bot-db"]["public"] || !params["bot-db"]["public"]["recipient"] || !params["bot-db"]["public"]["recipient"]["address"]) { + return { + markup: status.components.validationMessage( + "Wrong address", + "Recipient address must be specified" + ) + }; + } + if (!params["amount"]) { + return { + markup: status.components.validationMessage( + I18n.t('validation_title'), + I18n.t('validation_amount_specified') + ) + }; + } + + var amount = params.amount.replace(",", "."); + var amountSplitted = amount.split("."); + if (amountSplitted.length === 2 && amountSplitted[1].length > 18) { + return { + markup: status.components.validationMessage( + I18n.t('validation_title'), + I18n.t('validation_amount_is_too_small') + ) + }; + } + + if (isNaN(parseFloat(params.amount.replace(",", ".")))) { + return { + markup: status.components.validationMessage( + I18n.t('validation_title'), + I18n.t('validation_invalid_number') + ) + }; + } + try { - var val = web3.toWei(params.amount.replace(",", "."), "ether"); + var val = web3.toWei(amount, "ether"); if (val <= 0) { throw new Error(); } diff --git a/bots/wallet/translations.js b/bots/wallet/translations.js index 1e0c28f858..07c02bc14a 100644 --- a/bots/wallet/translations.js +++ b/bots/wallet/translations.js @@ -1,7 +1,20 @@ I18n.translations = { en: { - send_title: 'Send ETH', + send_title: 'Send transaction', send_description: 'Send a payment', + send_choose_recipient: 'Choose recipient', + send_specify_amount: 'Specify amount', + send_fee: 'Fee', + send_cheaper: 'Cheaper', + send_faster: 'Faster', + send_explanation: 'This is the most amount of money that might be used to process this transaction. Your transaction will be mined ', + send_explanation_0: 'in a few minutes or more.', + send_explanation_1: 'likely within a few minutes.', + send_explanation_2: 'usually within a minute.', + send_explanation_3: 'probably within 30 seconds.', + send_explanation_4: 'probably within a few seconds.', + + eth: 'ETH', request_title: 'Request ETH', request_description: 'Request a payment', @@ -14,7 +27,7 @@ I18n.translations = { validation_insufficient_amount: 'Insufficient funds for gas * price + value (balance ' }, ru: { - send_title: 'Отправить ETH', + send_title: 'Отправить транзакцию', send_description: 'Отправить платеж', request_title: 'Запросить ETH', diff --git a/externs/externs.js b/externs/externs.js index 909fa417f4..fa6c455b7a 100644 --- a/externs/externs.js +++ b/externs/externs.js @@ -4,11 +4,12 @@ var TopLevel = { "addEventListener" : function () {}, "addListener" : function () {}, "addOrientationListener" : function () {}, -"addSymKey" : function () {}, +"addSymmetricKeyFromPassword" : function () {}, "Alert" : function () {}, "alert" : function () {}, "Animated" : function () {}, "Array" : function () {}, +"AutoGrowingTextInput" : function () {}, "awesome-phonenumber" : function () {}, "blur" : function () {}, "call" : function () {}, @@ -16,6 +17,7 @@ var TopLevel = { "capture" : function () {}, "catch" : function () {}, "Chance" : function () {}, +"checkVideoAuthorizationStatus" : function () {}, "clear" : function () {}, "clearCookies" : function () {}, "clearInterval" : function () {}, @@ -26,7 +28,7 @@ var TopLevel = { "close" : function () {}, "closeDrawer" : function () {}, "codec" : function () {}, -"completeTransaction" : function () {}, +"completeTransactions" : function () {}, "console" : function () {}, "contentOffset" : function () {}, "contentSize" : function () {}, @@ -67,36 +69,34 @@ var TopLevel = { "fromUtf8" : function () {}, "fromWei" : function () {}, "generate" : function () {}, +"geoPermissionsGranted" : function () {}, "get" : function () {}, "getAll" : function () {}, "getBlock" : function () {}, "getCardId" : function () {}, "getExample" : function () {}, "getInitialOrientation" : function () {}, -"getIPAddress" : function () {}, "getLayout" : function () {}, "getNumber" : function () {}, "getSyncing" : function () {}, "getTime" : function () {}, "getTimezoneOffset" : function () {}, +"goBack" : function () {}, +"goForward" : function () {}, "goog" : function () {}, "guid" : function () {}, "hash" : function () {}, -"hasSymKey" : function () {}, "headers" : function () {}, "height" : function () {}, "hex" : function () {}, "hide" : function () {}, -"HttpProvider" : function () {}, "IBGLog" : function () {}, "initialPage" : function () {}, "initJail" : function () {}, "isAddress" : function () {}, "isConnected" : function () {}, "isMatches" : function () {}, -"isMobile" : function () {}, "isNaN" : function () {}, -"isValid" : function () {}, "Item" : function () {}, "JSON" : function () {}, "jsonEvent" : function () {}, @@ -113,13 +113,13 @@ var TopLevel = { "Math" : function () {}, "message" : function () {}, "moveFile" : function () {}, +"moveToInternalStorage" : function () {}, "moveY" : function () {}, "nativeEvent" : function () {}, "NativeModules" : function () {}, "Number" : function () {}, "objects" : function () {}, "ok" : function () {}, -"open" : function () {}, "openDrawer" : function () {}, "openPicker" : function () {}, "openURL" : function () {}, @@ -132,11 +132,11 @@ var TopLevel = { "parseInt" : function () {}, "parseJail" : function () {}, "path" : function () {}, +"PermissionsAndroid" : function () {}, "Platform" : function () {}, "post" : function () {}, "props" : function () {}, "prototype" : function () {}, -"providers" : function () {}, "push" : function () {}, "random" : function () {}, "randomBytes" : function () {}, @@ -149,8 +149,12 @@ var TopLevel = { "reload" : function () {}, "remove" : function () {}, "removeAllListeners" : function () {}, +"removeEventListener" : function () {}, +"requestMultiple" : function () {}, "require" : function () {}, "reset" : function () {}, +"resetOkHttpClient" : function () {}, +"respond" : function () {}, "reverse" : function () {}, "round" : function () {}, "schema" : function () {}, @@ -160,9 +164,11 @@ var TopLevel = { "scrollView" : function () {}, "selection" : function () {}, "sendToBridge" : function () {}, +"sendWeb3Request" : function () {}, "sequence" : function () {}, "set" : function () {}, "setInterval" : function () {}, +"setNativeProps" : function () {}, "setSoftInputMode" : function () {}, "setState" : function () {}, "setString" : function () {}, @@ -170,7 +176,10 @@ var TopLevel = { "setValue" : function () {}, "Sha256" : function () {}, "sha3" : function () {}, +"Share" : function () {}, +"share" : function () {}, "shh" : function () {}, +"shouldMoveToInternalStorage" : function () {}, "show" : function () {}, "showActionSheetWithOptions" : function () {}, "sjcl" : function () {}, @@ -197,7 +206,7 @@ var TopLevel = { "toAscii" : function () {}, "toBits" : function () {}, "toDecimal" : function () {}, -"toHex" : function () {}, +"toJSON" : function () {}, "toLocaleString" : function () {}, "toLowerCase" : function () {}, "toNumber" : function () {}, @@ -218,5 +227,6 @@ var TopLevel = { "writeTag" : function () {}, "x" : function () {}, "y" : function () {}, +"_bodyText" : function () {}, "_value" : function () {} } \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_back_gray.imageset/Contents.json b/ios/StatusIm/Images.xcassets/icon_back_gray.imageset/Contents.json new file mode 100644 index 0000000000..0e7627b6d7 --- /dev/null +++ b/ios/StatusIm/Images.xcassets/icon_back_gray.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "iconBackDark.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "iconBackDark@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "iconBackDark@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/StatusIm/Images.xcassets/icon_back_gray.imageset/iconBackDark.png b/ios/StatusIm/Images.xcassets/icon_back_gray.imageset/iconBackDark.png new file mode 100644 index 0000000000000000000000000000000000000000..3782cb217cf16cbb9a2b5564209d216198bb3b0b GIT binary patch literal 372 zcmV-)0gL{LP)Px$E=fc|R7ef&mAz`iFc5(6oIm2u3FHYkyZx6rf41FD zf=l5nw+iaOt%AC678qZVd@UbJL!D~QP<0puK#=6g)ao8mbFNBN=EhBphh~TfUqtYf zq?w4KxToxOnp&H@c>nBY$$rBUmicXO6o#)qPIQ@M6)>od#7V#sE)SQf>P?&lmr9IH zW~e&2_MYDwpBw*AvAV9CR}wH_8oYdSqY-cm`RBSaLw68uO#Tb52iF>JGI#z@^0000Px$e5m@!MkKorOC(n7_!sNkgd0sIu*)Q(Clv{(gqKZWkDF16SU78ju@ zZv84QPJ#{5kJ zw6BBP(=mz69PKc=_?b-KNai`g4G zuK$8>HEVqxYZw!tfX4ug2vETr5uk+EEhRlo+kHUV4kS_Evu7Z$J$Ur3?=qMzwAeyJ?radr)< zPZL5RSV^#12(Q*z(noPr7HBnUFPwP|+Y*a+ST!w79FxZTy!o`htm~E4T>K1bexCC4RE`huRegXUwHQG}^6gvO_002ovPDHLkV1h*+=|}(o literal 0 HcmV?d00001 diff --git a/ios/StatusIm/Images.xcassets/icon_back_gray.imageset/iconBackDark@3x.png b/ios/StatusIm/Images.xcassets/icon_back_gray.imageset/iconBackDark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0bcd6a9b088459b560d3264fd49ae102c2e92bba GIT binary patch literal 747 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!jKx9jP7LeL$-HD>U|Qgy{m#cain6c1_uUa!M z>Cw&I_6PnkbV>wFT%=-sF4(PW_SB{4X4mTPu}zt*`1{_T{B-jVW?m=Nn3NPy&^^{I z-#bn))^Dqp&(fM5y!m2acIs0u%ehI+Teg?~s4A%3x4P#ii$>JT$t%RxZoM|UeTIi% zT0#8A_XT?AcJr}6l1hE%9(m0#IQa9_^ca~>4*V9Ube?k=q&>{PRykk#yM=?)3(q;# zEo;oKsLRg?!ylf+n*zH5hx zEf4IE_FUx7T3RWs#_Gb$KIJ4s;1NY;7twYWfz&yt8dp3H;X1;&+(<#sVk$%7Ax^#{ zyy_YP?sF0xVhWoW3XeImKk94dGU2qyaM)k9_t*-1n+PhxUCeQlY`19_|k~p#2_DdFA7efBt-FB^N|B3)V4-E&;O(mbp-0J7Cs9c$6 z66022d9X_}%C~`IMxl)Oz3p9nyoP;TEUX3YPCyrmuyhy)87QptaBQ?tVr75S7Q!i@ zzT{NH4uw#l?^YHmyz)?K&`_SpxTwp8t3yf1QX$JjK{rMBk zVx5zsPt@X;g$kL*oy}cWXH$Iq$Gyohj_XbsX?xy{Ilt?D@*7WP$AkV#nWcAr3H?9V zP-u9P`ER#K=GMn*dH)a8tGZO=*RrI)r?Z=PKyo;!c`(UVh)G15yiOCICL X@^259^PO1&OuY=Au6{1-oD!M<&}%ss literal 0 HcmV?d00001 diff --git a/resources/status.js b/resources/status.js index 9fb9a326c4..4ccc139e45 100644 --- a/resources/status.js +++ b/resources/status.js @@ -178,6 +178,14 @@ function validationMessage(titleText, descriptionText) { }]; } +function chooseContact(titleText, botDbKey, argumentIndex) { + return ['choose-contact', { + title: titleText, + "bot-db-key": botDbKey, + index: argumentIndex + }]; +} + var status = { command: function (h) { var command = new Command(); @@ -208,7 +216,11 @@ var status = { }, events: { SET_VALUE: 'set-value', - SET_COMMAND_ARGUMENT: 'set-command-argument' + SET_COMMAND_ARGUMENT: 'set-command-argument', + UPDATE_DB: 'set', + SET_COMMAND_ARGUMENT_FROM_DB: 'set-command-argument-from-db', + SET_VALUE_FROM_DB: 'set-value-from-db', + FOCUS_INPUT: 'focus-input' }, actions: { WEB_VIEW_BACK: 'web-view-back', @@ -220,6 +232,7 @@ var status = { view: view, text: text, textInput: textInput, + slider: slider, image: image, qrCode: qrCode, linking: linking, @@ -230,6 +243,7 @@ var status = { webView: webView, validationMessage: validationMessage, bridgedWebView: bridgedWebView, + chooseContact: chooseContact, subscribe: subscribe, dispatch: dispatch }, diff --git a/src/status_im/bots/handlers.cljs b/src/status_im/bots/handlers.cljs index 0dc064f762..34de522c00 100644 --- a/src/status_im/bots/handlers.cljs +++ b/src/status_im/bots/handlers.cljs @@ -3,8 +3,17 @@ [status-im.components.status :as status] [status-im.utils.handlers :as u])) +(defn chats-with-bot [chats bot] + (reduce (fn [acc [_ {:keys [chat-id contacts]}]] + (let [contacts (map :identity contacts)] + (if (some #{bot} contacts) + (conj acc chat-id) + acc))) + [] + chats)) + (defn check-subscriptions - [{:keys [bot-db] :as db} [handler {:keys [path key bot]}]] + [{:keys [bot-db chats] :as db} [handler {:keys [path key bot]}]] (let [path' (or path [key]) subscriptions (get-in db [:bot-subscriptions path']) current-bot-db (get bot-db bot)] @@ -17,22 +26,23 @@ :function :subscription :parameters {:name name :subscriptions subs-values} - :callback #(re-frame/dispatch - [::calculated-subscription {:bot bot - :path [name] - :result %}])}))))) - -(u/register-handler - :set-bot-db - (re-frame/after check-subscriptions) - (fn [db [_ {:keys [bot key value]}]] - (assoc-in db [:bot-db bot key] value))) + :callback #(do + (re-frame/dispatch + [::calculated-subscription {:bot bot + :path [name] + :result %}]) + (doseq [chat-id (chats-with-bot chats bot)] + (re-frame/dispatch + [::calculated-subscription {:bot chat-id + :path [name] + :result %}])))}))))) (u/register-handler :set-in-bot-db (re-frame/after check-subscriptions) - (fn [db [_ {:keys [bot path value]}]] - (assoc-in db (concat [:bot-db bot] path) value))) + (fn [{:keys [current-chat-id] :as db} [_ {:keys [bot path value]}]] + (let [bot (or bot current-chat-id)] + (assoc-in db (concat [:bot-db bot] path) value)))) (u/register-handler :register-bot-subscription @@ -61,5 +71,12 @@ (u/register-handler :update-bot-db - (fn [app-db [_ {:keys [bot db]}]] - (update-in app-db [:bot-db bot] merge db))) + (fn [{:keys [current-chat-id] :as app-db} [_ {:keys [bot db]}]] + (let [bot (or bot current-chat-id)] + (update-in app-db [:bot-db bot] merge db)))) + +(u/register-handler + :clear-bot-db + (fn [{:keys [current-chat-id] :as app-db} [_ {:keys [bot]}]] + (let [bot (or bot current-chat-id)] + (assoc-in app-db [:bot-db bot] nil)))) \ No newline at end of file diff --git a/src/status_im/chat/handlers.cljs b/src/status_im/chat/handlers.cljs index 591b5538dc..1f8cc006fe 100644 --- a/src/status_im/chat/handlers.cljs +++ b/src/status_im/chat/handlers.cljs @@ -265,7 +265,7 @@ db' (-> db (assoc :current-chat-id chat-id) (assoc-in [:chats chat-id :was-opened?] true)) - commands-loaded? (get-in db [:contacts chat-id :commands-loaded]) + commands-loaded? (get-in db [:contacts chat-id :commands-loaded?]) bot-url (get-in db [:contacts chat-id :bot-url]) was-opened? (get-in db [:chats chat-id :was-opened?]) call-init-command #(when (and (not was-opened?) bot-url) diff --git a/src/status_im/chat/handlers/commands.cljs b/src/status_im/chat/handlers/commands.cljs index 4a98fa87f9..524ad72cfe 100644 --- a/src/status_im/chat/handlers/commands.cljs +++ b/src/status_im/chat/handlers/commands.cljs @@ -6,28 +6,37 @@ [status-im.commands.utils :as cu] [status-im.i18n :as i18n] [status-im.utils.platform :as platform] - [taoensso.timbre :as log])) + [taoensso.timbre :as log] + [clojure.string :as str])) + +(defn generate-context [{:keys [contacts current-account-id chats] :as db} chat-id to] + (merge {:platform platform/platform + :from current-account-id + :to to + :chat {:chat-id chat-id + :group-chat (get-in chats [chat-id :group-chat])}} + i18n/delimeters)) (handlers/register-handler :request-command-data (handlers/side-effect! - (fn [{:keys [contacts current-account-id] :as db} + (fn [{:keys [contacts current-account-id chats] :as db} [_ {{:keys [command params content-command type]} :content - :keys [message-id chat-id on-requested jail-id] :as message} data-type]] - (let [jail-id (or jail-id chat-id)] - (if-not (get-in contacts [jail-id :commands-loaded]) + :keys [message-id from chat-id on-requested jail-id] :as message} data-type]] + (let [jail-id (or jail-id chat-id) + jail-id' (if (get-in chats [jail-id :group-chat]) + (get-in chats [jail-id :command-suggestions (keyword command) :owner-id]) + jail-id)] + (if-not (get-in contacts [jail-id' :commands-loaded?]) (do (dispatch [:add-commands-loading-callback - jail-id + jail-id' #(dispatch [:request-command-data message data-type])]) - (dispatch [:load-commands! jail-id])) + (dispatch [:load-commands! jail-id'])) (let [path [(if (= :response (keyword type)) :responses :commands) (if content-command content-command command) data-type] to (get-in contacts [chat-id :address]) params {:parameters params - :context (merge {:platform platform/platform - :from current-account-id - :to to} - i18n/delimeters)} + :context (generate-context db chat-id to)} callback #(let [result (get-in % [:result :returned]) result (if (:markup result) (update result :markup cu/generate-hiccup) @@ -35,7 +44,7 @@ (dispatch [:set-in [:message-data data-type message-id] result]) (when on-requested (on-requested result)))] ;chat-id path params callback lock? type - (status/call-jail {:jail-id jail-id + (status/call-jail {:jail-id jail-id' :path path :params params :callback callback}))))))) diff --git a/src/status_im/chat/handlers/input.cljs b/src/status_im/chat/handlers/input.cljs index 770ccda55e..dc522c63bf 100644 --- a/src/status_im/chat/handlers/input.cljs +++ b/src/status_im/chat/handlers/input.cljs @@ -14,16 +14,20 @@ [clojure.string :as str])) (handlers/register-handler - :set-chat-input-text - (fn [{:keys [current-chat-id chats chat-ui-props] :as db} [_ text chat-id]] - (let [chat-id (or chat-id current-chat-id) - ends-with-space? (input-model/text-ends-with-space? text)] + :set-chat-input-text + (fn [{:keys [current-chat-id chats chat-ui-props] :as db} [_ text chat-id]] + (let [chat-id (or chat-id current-chat-id) + ends-with-space? (input-model/text-ends-with-space? text)] (dispatch [:update-suggestions chat-id text]) - (if-let [{command :command} (input-model/selected-chat-command db chat-id text)] + (->> text + (input-model/text->emoji) + (assoc-in db [:chats chat-id :input-text])) + + ;; TODO(alwx): need to understand the need in this + #_(if-let [{command :command} (input-model/selected-chat-command db chat-id text)] (let [{old-args :args} (input-model/selected-chat-command db chat-id) text-splitted (input-model/split-command-args text) - new-args (rest text-splitted) new-input-text (input-model/make-input-text text-splitted old-args)] (assoc-in db [:chats chat-id :input-text] new-input-text)) (->> text @@ -41,15 +45,20 @@ :select-chat-input-command (handlers/side-effect! (fn [{:keys [current-chat-id chat-ui-props] :as db} - [_ {:keys [prefill sequential-params] :as command} metadata prevent-auto-focus?]] + [_ {:keys [prefill prefill-bot-db sequential-params name] :as command} metadata prevent-auto-focus?]] (dispatch [:set-chat-input-text (str (chat-utils/command-name command) const/spacing-char (when-not sequential-params (input-model/join-command-args prefill)))]) + (dispatch [:clear-bot-db]) + (when prefill-bot-db + (dispatch [:update-bot-db {:bot current-chat-id + :db prefill-bot-db}])) (dispatch [:set-chat-input-metadata metadata]) (dispatch [:set-chat-ui-props {:show-suggestions? false :result-box nil - :validation-messages nil}]) + :validation-messages nil + :prev-command name}]) (dispatch [:load-chat-parameter-box command 0]) (if sequential-params (js/setTimeout @@ -69,14 +78,15 @@ (handlers/register-handler :set-command-argument (handlers/side-effect! - (fn [{:keys [current-chat-id] :as db} [_ [index arg]]] + (fn [{:keys [current-chat-id] :as db} [_ [index arg move-to-next?]]] (let [command (-> (get-in db [:chats current-chat-id :input-text]) (input-model/split-command-args)) seq-params? (-> (input-model/selected-chat-command db current-chat-id) (get-in [:command :sequential-params]))] (if seq-params? (dispatch [:set-chat-seq-arg-input-text arg]) - (let [command-name (first command) + (let [arg (str/replace arg (re-pattern const/arg-wrapping-char) "") + command-name (first command) command-args (into [] (rest command)) command-args (if (< index (count command-args)) (assoc command-args index arg) @@ -84,7 +94,9 @@ (dispatch [:set-chat-input-text (str command-name const/spacing-char (input-model/join-command-args command-args) - const/spacing-char)]))))))) + (when (and move-to-next? + (= index (dec (count command-args)))) + const/spacing-char))]))))))) (handlers/register-handler :chat-input-focus @@ -104,25 +116,28 @@ requests (->> (suggestions/get-request-suggestions db chat-text) (remove (fn [{:keys [type]}] (= type :grant-permissions)))) - suggestions (suggestions/get-command-suggestions db chat-text) + commands (suggestions/get-command-suggestions db chat-text) global-commands (suggestions/get-global-command-suggestions db chat-text) + all-commands (->> (into global-commands commands) + (remove (fn [[k {:keys [hidden?]}]] hidden?)) + (into {})) {:keys [dapp?]} (get-in db [:contacts chat-id])] (if (and dapp? (str/blank? chat-text)) (dispatch [:set-in [:chats chat-id :parameter-boxes :message] nil]) (dispatch [::check-dapp-suggestions chat-id chat-text])) (-> db (assoc-in [:chats chat-id :request-suggestions] requests) - (assoc-in [:chats chat-id :command-suggestions] (into suggestions global-commands)))))) + (assoc-in [:chats chat-id :command-suggestions] all-commands))))) (handlers/register-handler :load-chat-parameter-box (handlers/side-effect! - (fn [{:keys [current-chat-id current-account-id] :as db} - [_ {:keys [name type bot] :as command}]] + (fn [{:keys [current-chat-id bot-db current-account-id] :as db} + [_ {:keys [name type bot owner-id] :as command}]] (let [parameter-index (input-model/argument-position db current-chat-id)] (when (and command (> parameter-index -1)) - (let [jail-id (or bot current-chat-id) - data (get-in db [:local-storage current-chat-id]) + (let [data (get-in db [:local-storage current-chat-id]) + bot-db (get bot-db (or bot current-chat-id)) path [(if (= :command type) :commands :responses) name :params @@ -132,13 +147,14 @@ (input-model/split-command-args) (rest)) to (get-in db [:contacts current-chat-id :address]) - params {:parameters {:args args} + params {:parameters {:args args + :bot-db bot-db} :context (merge {:data data :from current-account-id :to to} (input-model/command-dependent-context-params command))}] (status/call-jail - {:jail-id jail-id + {:jail-id (or bot owner-id current-chat-id) :path path :params params :callback #(dispatch [:received-bot-response @@ -239,28 +255,33 @@ (handlers/register-handler ::request-command-data (handlers/side-effect! - (fn [{:keys [contacts] :as db} - [_ {{:keys [command metadata args] :as c} :command - :keys [message-id chat-id jail-id data-type after]}]] + (fn [{:keys [contacts bot-db] :as db} + [_ {{:keys [command + metadata + args] + :as c} :command + :keys [message-id chat-id jail-id data-type after]}]] (let [{:keys [dapp? dapp-url name]} (get contacts chat-id) message-id (random/id) metadata (merge metadata (when dapp? {:url (i18n/get-contact-translated chat-id :dapp-url dapp-url) :name (i18n/get-contact-translated chat-id :name name)})) - params (input-model/args->params c) + owner-id (:owner-id command) + bot-db (get bot-db chat-id) + params (assoc (input-model/args->params c) :bot-db bot-db) command-message {:command command :params params :to-message (:to-message-id metadata) :created-at (time/now-ms) :id message-id :chat-id chat-id - :jail-id jail-id} + :jail-id (or owner-id jail-id)} request-data {:message-id message-id :chat-id chat-id - :jail-id jail-id + :jail-id (or owner-id jail-id) :content {:command (:name command) - :params (assoc params :metadata metadata) + :params (assoc params :metadata metadata :bot-db bot-db) :type (:type command)} :on-requested #(after command-message %)}] (dispatch [:request-command-data request-data data-type]))))) @@ -342,3 +363,48 @@ (fn [{:keys [current-chat-id] :as db} [_ text chat-id]] (let [chat-id (or chat-id current-chat-id)] (assoc-in db [:chats chat-id :seq-argument-input-text] text)))) + +(handlers/register-handler + :update-text-selection + (handlers/side-effect! + (fn [{:keys [current-chat-id] :as db} [_ selection]] + (let [input-text (get-in db [:chats current-chat-id :input-text]) + command (input-model/selected-chat-command db current-chat-id input-text)] + (when (and (= selection (+ (count const/command-char) + (count (get-in command [:command :name])) + (count const/spacing-char))) + (get-in command [:command :sequential-params])) + (dispatch [:chat-input-focus :seq-input-ref])) + (dispatch [:set-chat-ui-props {:selection selection}]) + (dispatch [:load-chat-parameter-box (:command command)]))))) + +(handlers/register-handler + :select-prev-argument + (handlers/side-effect! + (fn [{:keys [chat-ui-props current-chat-id] :as db} _] + (let [arg-pos (input-model/argument-position db current-chat-id)] + (when (pos? arg-pos) + (let [input-text (get-in db [:chats current-chat-id :input-text]) + new-sel (->> (input-model/split-command-args input-text) + (take (inc arg-pos)) + (input-model/join-command-args) + (count)) + ref (get-in chat-ui-props [current-chat-id :input-ref])] + (.setNativeProps ref (clj->js {:selection {:start new-sel :end new-sel}})) + (dispatch [:update-text-selection new-sel]))))))) + +(handlers/register-handler + :select-next-argument + (handlers/side-effect! + (fn [{:keys [chat-ui-props current-chat-id] :as db} _] + (let [arg-pos (input-model/argument-position db current-chat-id)] + (let [input-text (get-in db [:chats current-chat-id :input-text]) + command-args (cond-> (input-model/split-command-args input-text) + (input-model/text-ends-with-space? input-text) (conj "")) + new-sel (->> command-args + (take (+ 3 arg-pos)) + (input-model/join-command-args) + (count)) + ref (get-in chat-ui-props [current-chat-id :input-ref])] + (.setNativeProps ref (clj->js {:selection {:start new-sel :end new-sel}})) + (dispatch [:update-text-selection new-sel])))))) \ No newline at end of file diff --git a/src/status_im/chat/handlers/receive_message.cljs b/src/status_im/chat/handlers/receive_message.cljs index 2bd8c2c821..68853d1110 100644 --- a/src/status_im/chat/handlers/receive_message.cljs +++ b/src/status_im/chat/handlers/receive_message.cljs @@ -97,7 +97,7 @@ (assoc-in db [:chats chat-id :last-message] message))) (defn commands-loaded? [db chat-id] - (get-in db [:contacts chat-id :commands-loaded])) + (get-in db [:contacts chat-id :commands-loaded?])) (def timeout 400) diff --git a/src/status_im/chat/handlers/send_message.cljs b/src/status_im/chat/handlers/send_message.cljs index eb32dbc98b..f387ec4a79 100644 --- a/src/status_im/chat/handlers/send_message.cljs +++ b/src/status_im/chat/handlers/send_message.cljs @@ -128,19 +128,22 @@ (register-handler ::invoke-command-handlers! (u/side-effect! - (fn [db [_ {:keys [chat-id address command-message] - :as parameters}]] + (fn [{:keys [bot-db accounts current-account-id] :as db} + [_ {:keys [chat-id address command-message] + :as parameters}]] (let [{:keys [id command params]} command-message - {:keys [type name bot]} command + {:keys [type name bot owner-id]} command path [(if (= :command type) :commands :responses) name :handler] to (get-in db [:contacts chat-id :address]) - params {:parameters params - :context {:from address - :to to - :message-id id}} - identity (or bot chat-id)] + identity (or owner-id bot chat-id) + bot-db (get bot-db (or bot chat-id)) + params {:parameters (assoc params :bot-db bot-db) + :context {:from address + :to to + :current-account (get accounts current-account-id) + :message-id id}}] (dispatch [:check-and-load-commands! identity diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs index c424c07db2..8ceebe1ca4 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -26,29 +26,35 @@ (dec (count text))))) (defn possible-chat-actions [{:keys [global-commands] :as db} chat-id] - (let [{:keys [requests]} (get-in db [:chats chat-id]) - {:keys [commands responses]} (get-in db [:contacts chat-id]) - - commands' (into {} (map (fn [[k v]] [k [v :any]]) (merge global-commands commands))) - responses' (into {} (map (fn [{:keys [message-id type]}] - [type [(get responses type) message-id]]) - requests))] - (vals (merge commands' responses')))) + (let [{:keys [contacts requests]} (get-in db [:chats chat-id])] + (->> contacts + (map (fn [{:keys [identity]}] + (let [{:keys [commands responses]} (get-in db [:contacts identity])] + (let [commands' (mapv (fn [[k v]] [k [v :any]]) (merge global-commands commands)) + responses' (mapv (fn [{:keys [message-id type]}] + [type [(get responses type) message-id]]) + requests)] + (into commands' responses'))))) + (reduce (fn [m cur] (into (or m {}) cur))) + (into {}) + (vals)))) (defn split-command-args [command-text] - (let [space? (text-ends-with-space? command-text) - command-text (if space? - (str command-text ".") - command-text) - command-text-normalized (if command-text (str/replace (str/trim command-text) #" +" " ") command-text) - splitted (cond-> (str/split command-text-normalized const/spacing-char) - space? (drop-last))] + (let [space? (text-ends-with-space? command-text) + command-text (if space? + (str command-text ".") + command-text) + command-text-normalized (if command-text + (str/replace (str/trim command-text) #" +" " ") + command-text) + splitted (cond-> (str/split command-text-normalized const/spacing-char) + space? (drop-last))] (->> splitted (reduce (fn [[list command-started?] arg] (let [quotes-count (count (filter #(= % const/arg-wrapping-char) arg)) has-quote? (and (= quotes-count 1) (str/index-of arg const/arg-wrapping-char)) - arg (str/replace arg #"\"" "") + arg (str/replace arg (re-pattern const/arg-wrapping-char) "") new-list (if command-started? (let [index (dec (count list))] (update list index str const/spacing-char arg)) @@ -92,27 +98,35 @@ ([{:keys [current-chat-id] :as db} chat-id] (selected-chat-command db chat-id (get-in db [:chats chat-id :input-text])))) +(def *no-argument-error* -1) + (defn current-chat-argument-position - [{:keys [args] :as command} input-text seq-arguments] + [{:keys [args] :as command} input-text selection seq-arguments] (if command - (let [args-count (count args)] - (cond - (:sequential-params command) - (count seq-arguments) - - (= (last input-text) const/spacing-char) - args-count - - :default - (dec args-count))) - -1)) + (if (get-in command [:command :sequential-params]) + (count seq-arguments) + (let [subs-input-text (subs input-text 0 selection)] + (if subs-input-text + (let [args (split-command-args subs-input-text) + argument-index (dec (count args)) + ends-with-space? (text-ends-with-space? subs-input-text) + arg-wrapping-count (-> (frequencies subs-input-text) + (get const/arg-wrapping-char) + (or 0))] + (if (and ends-with-space? + (even? arg-wrapping-count)) + argument-index + (dec argument-index))) + *no-argument-error*))) + *no-argument-error*)) (defn argument-position [{:keys [current-chat-id] :as db} chat-id] (let [chat-id (or chat-id current-chat-id) input-text (get-in db [:chats chat-id :input-text]) seq-arguments (get-in db [:chats chat-id :seq-arguments]) + selection (get-in db [:chat-ui-props chat-id :selection]) chat-command (selected-chat-command db chat-id)] - (current-chat-argument-position chat-command input-text seq-arguments))) + (current-chat-argument-position chat-command input-text selection seq-arguments))) (defn command-completion ([{:keys [current-chat-id] :as db} chat-id] @@ -189,4 +203,4 @@ (str command const/spacing-char - (str/join const/spacing-char new-args)))) + (join-command-args new-args)))) \ No newline at end of file diff --git a/src/status_im/chat/models/suggestions.cljs b/src/status_im/chat/models/suggestions.cljs index 5713b9b23e..cace95cf64 100644 --- a/src/status_im/chat/models/suggestions.cljs +++ b/src/status_im/chat/models/suggestions.cljs @@ -28,10 +28,16 @@ (defn get-command-suggestions [{:keys [current-chat-id] :as db} text] - (let [commands (get-in db [:contacts current-chat-id :commands])] - (filter (fn [[_ v]] ((can-be-suggested? text) v)) commands))) + (->> (get-in db [:chats current-chat-id :contacts]) + (map (fn [{:keys [identity]}] + (let [commands (get-in db [:contacts identity :commands])] + (->> commands + (filter (fn [[_ v]] ((can-be-suggested? text) v))))))) + (reduce (fn [m cur] (into (or m {}) cur))) + (into {}))) (defn get-global-command-suggestions [{:keys [global-commands] :as db} text] - (filter (fn [[_ v]] ((can-be-suggested? chat-consts/bot-char :bot text) v)) - global-commands)) + (->> global-commands + (filter (fn [[_ v]] ((can-be-suggested? chat-consts/bot-char :bot text) v))) + (into {}))) diff --git a/src/status_im/chat/styles/input/box_header.cljs b/src/status_im/chat/styles/input/box_header.cljs new file mode 100644 index 0000000000..42f564742f --- /dev/null +++ b/src/status_im/chat/styles/input/box_header.cljs @@ -0,0 +1,41 @@ +(ns status-im.chat.styles.input.box-header + (:require [status-im.components.styles :as common])) + +(def header-height 33) + +(def header-container + {:background-color common/color-white + :alignItems :center + :justifyContent :center + :height header-height}) + +(def header-title-container + {:flex-direction :row + :height header-height + :border-bottom-color "rgba(193, 199, 203, 0.28)" + :border-bottom-width 1}) + +(defn header-title-text [back?] + {:color common/color-black + :flex 1 + :font-size 15 + :text-align :center + :padding-top 0 + :margin-left (if back? 32 72) + :margin-right 32}) + +(def header-back-container + {:width 24 + :height 24 + :margin-left 16 + :top -4}) + +(def header-close-container + {:width 24 + :height 24 + :margin-right 16 + :top -4}) + +(def header-icon + {:width 24 + :height 24}) \ No newline at end of file diff --git a/src/status_im/chat/styles/input/result_box.cljs b/src/status_im/chat/styles/input/result_box.cljs index 99c1d6143d..3793073cb8 100644 --- a/src/status_im/chat/styles/input/result_box.cljs +++ b/src/status_im/chat/styles/input/result_box.cljs @@ -15,31 +15,3 @@ :bottom bottom :position :absolute}) -(def header-container - {:background-color common/color-white - :alignItems :center - :justifyContent :center - :height 35}) - -(def header-title-container - {:margin-bottom 15 - :flex-direction :row}) - -(def header-title-text - {:color common/color-black - :flex 1 - :font-size 15 - :text-align :center - :padding-top 1 - :margin-left 72 - :margin-right 32}) - -(def header-close-container - {:width 24 - :height 24 - :margin-right 16 - :top -3}) - -(def header-close-icon - {:width 24 - :height 24}) diff --git a/src/status_im/chat/subs.cljs b/src/status_im/chat/subs.cljs index 05856e9493..53afb86d5c 100644 --- a/src/status_im/chat/subs.cljs +++ b/src/status_im/chat/subs.cljs @@ -70,6 +70,18 @@ (let [current-chat (or chat-id (@db :current-chat-id))] (reaction (or (get-in @db [:contacts current-chat :responses]) {}))))) +(register-sub + :get-commands-and-responses + (fn [db [_ chat-id]] + (reaction + (let [{:keys [chats contacts]} @db] + (->> (get-in chats [chat-id :contacts]) + (filter :is-in-chat) + (mapv (fn [{:keys [identity]}] + (let [{:keys [commands responses]} (get contacts identity)] + (merge responses commands)))) + (apply merge)))))) + (register-sub :possible-chat-actions (fn [db [_ chat-id]] @@ -92,9 +104,10 @@ (fn [db] (let [command (subscribe [:selected-chat-command]) input-text (subscribe [:chat :input-text]) - seq-arguments (subscribe [:chat :seq-arguments])] + seq-arguments (subscribe [:chat :seq-arguments]) + selection (subscribe [:chat-ui-props :selection])] (reaction - (input-model/current-chat-argument-position @command @input-text @seq-arguments))))) + (input-model/current-chat-argument-position @command @input-text @selection @seq-arguments))))) (register-sub :chat-parameter-box @@ -104,7 +117,7 @@ index (subscribe [:current-chat-argument-position])] (reaction (cond - (and @command (> @index -1)) + (and @command (not= @index input-model/*no-argument-error*)) (let [command-name (get-in @command [:command :name])] (get-in @db [:chats @chat-id :parameter-boxes command-name @index])) @@ -188,8 +201,10 @@ (fn [db [_ chat-id]] (reaction (let [{:keys [last-message messages]} (get-in @db [:chats chat-id])] - (first - (sort-by :clock-value > (conj messages last-message))))))) + (->> (conj messages last-message) + (sort-by :clock-value >) + (filter :show?) + (first)))))) (register-sub :get-last-message-short-preview (fn [db [_ chat-id]] diff --git a/src/status_im/chat/views/choosers/choose_contact.cljs b/src/status_im/chat/views/choosers/choose_contact.cljs new file mode 100644 index 0000000000..408af9ca33 --- /dev/null +++ b/src/status_im/chat/views/choosers/choose_contact.cljs @@ -0,0 +1,47 @@ +(ns status-im.chat.views.choosers.choose-contact + (:require-macros [status-im.utils.views :refer [defview]]) + (:require [reagent.core :as r] + [re-frame.core :refer [dispatch subscribe]] + [clojure.string :as str] + [status-im.chat.constants :as const] + [status-im.components.react :refer [view + text + list-view + list-item]] + [status-im.components.contact.contact :refer [contact-view]] + [status-im.components.renderers.renderers :as renderers] + [status-im.utils.listview :as lw])) + +(defn- select-contact [arg-index bot-db-key {:keys [name] :as contact}] + (let [contact (select-keys contact [:address :whisper-identity :name :photo-path :dapp?]) + name (str/replace name (re-pattern const/arg-wrapping-char) "")] + (dispatch [:set-command-argument [arg-index name true]]) + (dispatch [:set-in-bot-db {:path [:public (keyword bot-db-key)] + :value contact}]) + (dispatch [:select-next-argument]))) + +(defn render-row [arg-index bot-db-key] + (fn [contact _ _] + (list-item + ^{:key contact} + [contact-view {:contact contact + :on-press #(select-contact arg-index bot-db-key contact)}]))) + +(defview choose-contact-view [{title :title + arg-index :index + bot-db-key :bot-db-key}] + [contacts [:contacts-filtered :people-in-current-chat]] + [view {:flex 1} + [text {:style {:font-size 14 + :color "rgb(147, 155, 161)" + :padding-top 12 + :padding-left 16 + :padding-right 16 + :padding-bottom 12}} + title] + [list-view {:dataSource (lw/to-datasource contacts) + :enableEmptySections true + :renderRow (render-row arg-index bot-db-key) + :bounces false + :keyboardShouldPersistTaps :always + :renderSeparator renderers/list-separator-renderer}]]) \ No newline at end of file diff --git a/src/status_im/chat/views/input/box_header.cljs b/src/status_im/chat/views/input/box_header.cljs new file mode 100644 index 0000000000..bf59bbaf3c --- /dev/null +++ b/src/status_im/chat/views/input/box_header.cljs @@ -0,0 +1,39 @@ +(ns status-im.chat.views.input.box-header + (:require-macros [status-im.utils.views :refer [defview]]) + (:require [re-frame.core :refer [subscribe dispatch]] + [status-im.components.react :refer [view + touchable-highlight + text + icon]] + [status-im.chat.styles.input.box-header :as style] + [taoensso.timbre :as log])) + +(defn get-header [type] + (fn [] + (let [parameter-box (subscribe [:chat-parameter-box]) + result-box (subscribe [:chat-ui-props :result-box]) + chat-id (subscribe [:get-current-chat-id]) + command (subscribe [:selected-chat-command]) + index (subscribe [:current-chat-argument-position])] + (fn [] + (let [{:keys [showBack title]} (if (= type :parameter-box) + @parameter-box + @result-box)] + [view {:style style/header-container} + [view style/header-title-container + (when showBack + [touchable-highlight {:on-press #(dispatch [:select-prev-argument])} + [view style/header-back-container + [icon :back_gray style/header-icon]]]) + [text {:style (style/header-title-text showBack) + :number-of-lines 1 + :font :medium} + title] + [touchable-highlight + {:on-press (fn [] + (if (= type :parameter-box) + (let [command-name (get-in @command [:command :name])] + (dispatch [:set-in [:chats @chat-id :parameter-boxes command-name @index] nil])) + (dispatch [:set-chat-ui-props {:result-box nil}])))} + [view style/header-close-container + [icon :close_light_gray style/header-icon]]]]]))))) \ No newline at end of file diff --git a/src/status_im/chat/views/input/input.cljs b/src/status_im/chat/views/input/input.cljs index e532d55595..086437f432 100644 --- a/src/status_im/chat/views/input/input.cljs +++ b/src/status_im/chat/views/input/input.cljs @@ -64,6 +64,7 @@ command (subscribe [:selected-chat-command]) sending-in-progress? (subscribe [:chat-ui-props :sending-in-progress?]) input-focused? (subscribe [:chat-ui-props :input-focused?]) + prev-command (subscribe [:chat-ui-props :prev-command]) input-ref (atom nil)] (fn [{:keys [set-layout-height set-container-width height single-line-input?]}] [text-input @@ -94,23 +95,26 @@ (dispatch [:set-chat-input-text text]) (if @command (do + (when (not= @prev-command (-> @command :command :name)) + (dispatch [:clear-bot-db @command])) (dispatch [:load-chat-parameter-box (:command @command)]) (dispatch [:set-chat-ui-props {:validation-messages nil}])) (do (dispatch [:set-chat-input-metadata nil]) - (dispatch [:set-chat-ui-props {:result-box nil - :validation-messages nil}])))))) + (dispatch [:set-chat-ui-props + {:result-box nil + :validation-messages nil + :prev-command (-> @command :command :name)}])))))) :on-content-size-change (when (and (not @input-focused?) (not single-line-input?)) #(let [h (-> (.-nativeEvent %) (.-contentSize) (.-height))] (set-layout-height h))) - :on-selection-change #(let [s (-> (.-nativeEvent %) - (.-selection))] - (when (and (= (.-end s) (+ 2 (count (get-in @command [:command :name])))) - (get-in @command [:command :sequential-params])) - (dispatch [:chat-input-focus :seq-input-ref]))) + :on-selection-change #(let [s (-> (.-nativeEvent %) + (.-selection)) + end (.-end s)] + (dispatch [:update-text-selection end])) :style (style/input-view height single-line-input?) :placeholder-text-color style/color-input-helper-placeholder :auto-capitalize :sentences}]))) diff --git a/src/status_im/chat/views/input/parameter_box.cljs b/src/status_im/chat/views/input/parameter_box.cljs index 04d8c3953e..c63df7b589 100644 --- a/src/status_im/chat/views/input/parameter_box.cljs +++ b/src/status_im/chat/views/input/parameter_box.cljs @@ -1,27 +1,23 @@ (ns status-im.chat.views.input.parameter-box (:require-macros [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe dispatch]] - [status-im.components.react :refer [view - scroll-view - touchable-highlight - text - icon]] [status-im.chat.views.input.animations.expandable :refer [expandable-view]] - [status-im.chat.views.input.utils :as input-utils] + [status-im.chat.views.input.box-header :as box-header] [status-im.commands.utils :as command-utils] - [status-im.i18n :refer [label]] - [taoensso.timbre :as log] - [clojure.string :as str])) + [taoensso.timbre :as log])) (defview parameter-box-container [] - [parameter-box [:chat-parameter-box] + [{:keys [markup]} [:chat-parameter-box] bot-db [:current-bot-db]] - (when (:hiccup parameter-box) - (command-utils/generate-hiccup (:hiccup parameter-box) bot-db))) + (when markup + (command-utils/generate-hiccup markup bot-db))) (defview parameter-box-view [] - [show-parameter-box? [:show-parameter-box?]] + [show-parameter-box? [:show-parameter-box?] + {:keys [title]} [:chat-parameter-box]] (when show-parameter-box? - [expandable-view {:key :parameter-box - :draggable? true} + [expandable-view {:key :parameter-box + :draggable? true + :custom-header (when title + (box-header/get-header :parameter-box))} [parameter-box-container]])) diff --git a/src/status_im/chat/views/input/result_box.cljs b/src/status_im/chat/views/input/result_box.cljs index bce6975492..8fbd9d47ec 100644 --- a/src/status_im/chat/views/input/result_box.cljs +++ b/src/status_im/chat/views/input/result_box.cljs @@ -7,23 +7,8 @@ text icon]] [status-im.chat.views.input.animations.expandable :refer [expandable-view]] - [status-im.chat.styles.input.result-box :as style] - [status-im.chat.views.input.utils :as input-utils] - [status-im.components.sync-state.offline :refer [offline-view]] - [status-im.i18n :refer [label]] - [taoensso.timbre :as log])) - -(defview header [] - [{:keys [title]} [:chat-ui-props :result-box]] - [view {:style style/header-container} - [view style/header-title-container - [text {:style style/header-title-text - :number-of-lines 1 - :font :medium} - title] - [touchable-highlight {:on-press #(dispatch [:set-chat-ui-props {:result-box nil}])} - [view style/header-close-container - [icon :close_light_gray style/header-close-icon]]]]]) + [status-im.chat.views.input.box-header :as box-header] + [status-im.components.sync-state.offline :refer [offline-view]])) (defview result-box-container [markup] [view {:flex 1} @@ -34,6 +19,6 @@ (when result-box [expandable-view {:key :result-box :draggable? true - :custom-header header} + :custom-header (box-header/get-header :result-box)} [result-box-container markup] [offline-view]])) diff --git a/src/status_im/chat/views/input/web_view.cljs b/src/status_im/chat/views/input/web_view.cljs index 0709eafec2..ff5fa29823 100644 --- a/src/status_im/chat/views/input/web_view.cljs +++ b/src/status_im/chat/views/input/web_view.cljs @@ -15,7 +15,7 @@ (let [{:strs [loading url title canGoBack canGoForward]} (js->clj event)] (when-not (= "about:blank" url) (when-not loading - (dispatch [:set-command-argument [0 url]])) + (dispatch [:set-command-argument [0 url false]])) (let [result-box (assoc result-box :can-go-back? canGoBack :can-go-forward? canGoForward) result-box (if (and dynamicTitle (not (str/blank? title))) (assoc result-box :title title) @@ -48,4 +48,5 @@ :on-navigation-state-change #(on-navigation-change % result-box) :local-storage-enabled true :start-in-loading-state true - :render-loading #(r/as-element [components/activity-indicator {:animating true}])}])) + :render-loading #(r/as-element [view {:padding-top 16} + [components/activity-indicator {:animating true}]])}])) diff --git a/src/status_im/chat/views/message/message.cljs b/src/status_im/chat/views/message/message.cljs index ee4eae0655..09a2f9f635 100644 --- a/src/status_im/chat/views/message/message.cljs +++ b/src/status_im/chat/views/message/message.cljs @@ -1,6 +1,7 @@ (ns status-im.chat.views.message.message (:require-macros [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe dispatch]] + [clojure.walk :as walk] [reagent.core :as r] [status-im.i18n :refer [message-status-label]] [status-im.components.react :refer [view @@ -93,15 +94,13 @@ (defn wallet-command-preview [{{:keys [name]} :contact-chat :keys [contact-address params outgoing? current-chat-id]}] - (let [amount (if (= 1 (count params)) - (first (vals params)) - (str params))] + (let [{:keys [recipient amount]} (walk/keywordize-keys params)] [text {:style st/command-text :font :default} (if (= current-chat-id wallet-chat-id) (let [label-val (if outgoing? :t/chat-send-eth-to :t/chat-send-eth-from)] (label label-val {:amount amount - :chat-name (or name contact-address)})) + :chat-name (or name contact-address recipient)})) (label :t/chat-send-eth {:amount amount}))])) (defn wallet-command? [content-type] @@ -122,18 +121,21 @@ (first (vals params)) (str params))])) +(defn commands-subscription [{:keys [type]}] + (if (= type "response") + :get-responses + :get-commands)) + (defview message-content-command [{:keys [message-id content content-type chat-id to from outgoing] :as message}] - [commands [(if (= (:type content) "response") - :get-responses - :get-commands) - chat-id] + [commands [:get-commands-and-responses chat-id] + from-commands [:get-commands-and-responses from] global-commands [:get :global-commands] current-chat-id [:get-current-chat-id] contact-chat [:get-in [:chats (if outgoing to from)]] preview [:get-in [:message-data :preview message-id :markup]]] - (let [{:keys [command params]} - (parse-command-message-content commands global-commands content) + (let [commands (merge commands from-commands) + {:keys [command params]} (parse-command-message-content commands global-commands content) {:keys [name type] icon-path :icon} command] [view st/content-command-view diff --git a/src/status_im/chat/views/message/request_message.cljs b/src/status_im/chat/views/message/request_message.cljs index 5451191848..81119c2469 100644 --- a/src/status_im/chat/views/message/request_message.cljs +++ b/src/status_im/chat/views/message/request_message.cljs @@ -70,19 +70,20 @@ [icon command-icon st/command-request-image])]]))}))) (defn message-content-command-request - [{:keys [message-id _ _ _]}] - (let [commands-atom (subscribe [:get-responses]) + [{:keys [message-id chat-id]}] + (let [commands-atom (subscribe [:get-commands-and-responses chat-id]) answered? (subscribe [:is-request-answered? message-id]) status-initialized? (subscribe [:get :status-module-initialized?]) markup (subscribe [:get-in [:message-data :preview message-id :markup]])] (fn [{:keys [message-id content from incoming-group]}] - (let [commands @commands-atom - params (:params content) - text-content (:text content) + (let [commands @commands-atom + {:keys [prefill prefillBotDb params] + text-content :text} content {:keys [command content]} (parse-command-request commands content) - command (if (and params command) - (merge command {:prefill (vals params)}) - command)] + command (if (and params command) + (merge command {:prefill prefill + :prefill-bot-db prefillBotDb}) + command)] [view st/comand-request-view [touchable-highlight {:on-press (when (and (not @answered?) @status-initialized?) diff --git a/src/status_im/commands/handlers/jail.cljs b/src/status_im/commands/handlers/jail.cljs index ba43d8f069..1e8cacd3ab 100644 --- a/src/status_im/commands/handlers/jail.cljs +++ b/src/status_im/commands/handlers/jail.cljs @@ -38,15 +38,14 @@ (defn suggestions-handler! [{:keys [contacts chats] :as db} [{:keys [chat-id default-db command parameter-index result]}]] - (let [returned (get-in result [:result :returned]) + (let [{:keys [markup] :as returned} (get-in result [:result :returned]) contains-markup? (contains? returned :markup) - markup (get returned :markup) - {:keys [dapp? dapp-url]} (get contacts chat-id) - path (if command - [:chats chat-id :parameter-boxes (:name command) parameter-index] - [:chats chat-id :parameter-boxes :message])] + path (if command + [:chats chat-id :parameter-boxes (:name command) parameter-index] + [:chats chat-id :parameter-boxes :message])] + (dispatch [:choose-predefined-expandable-height :parameter-box :default]) (when (and contains-markup? (not= (get-in db path) markup)) - (dispatch [:set-in path (when markup {:hiccup markup})]) + (dispatch [:set-in path returned]) (when default-db (dispatch [:update-bot-db {:bot chat-id :db default-db}]))))) @@ -56,17 +55,27 @@ (log/debug "Suggestion event: " n (first data) val) (let [{:keys [dapp?]} (get-in db [:contacts current-chat-id])] (case (keyword n) - :set-command-argument (dispatch [:set-command-argument (first data)]) - :set-value (dispatch [:set-chat-input-text (first data)]) - :set (let [opts {:bot current-chat-id - :path (mapv keyword data) - :value val}] - (dispatch [:set-in-bot-db opts])) + :set-command-argument + (let [[index value move-to-next?] (first data)] + (dispatch [:set-command-argument [index value move-to-next?]])) + :set-value + (dispatch [:set-chat-input-text (first data)]) + :set + (let [opts {:bot current-chat-id + :path (mapv keyword data) + :value val}] + (dispatch [:set-in-bot-db opts])) + :set-command-argument-from-db + (let [[index arg move-to-next?] (first data) + path (keyword arg) + value (str (get-in bot-db [current-chat-id path]))] + (dispatch [:set-command-argument [index value move-to-next?]])) :set-value-from-db (let [path (keyword (first data)) value (str (get-in bot-db [current-chat-id path]))] (dispatch [:set-chat-input-text value])) - ;; todo show error? + :focus-input + (dispatch [:chat-input-focus :input-ref]) nil))) (defn print-error-message! [message] diff --git a/src/status_im/commands/handlers/loading.cljs b/src/status_im/commands/handlers/loading.cljs index 9b8e74aba7..4d2f3f3c10 100644 --- a/src/status_im/commands/handlers/loading.cljs +++ b/src/status_im/commands/handlers/loading.cljs @@ -17,26 +17,23 @@ [status-im.chat.sign-up :as sign-up] [status-im.bots.constants :as bots-constants] [status-im.utils.datetime :as time] - [status-im.data-store.local-storage :as local-storage])) + [status-im.data-store.local-storage :as local-storage] + [clojure.string :as str])) (defn load-commands! - [{:keys [current-chat-id contacts]} [contact callback]] - (let [whole-contact? (map? contact) - {:keys [whisper-identity]} contact - identity' (or whisper-identity contact current-chat-id) - contact' (if whole-contact? - contact - (or (get contacts identity') - sign-up/console-contact))] - (when identity' - (dispatch [::fetch-commands! {:contact contact' - :callback callback}]))) - ;; todo uncomment - #_(if-let [{:keys [file]} (commands/get-by-chat-id contact)] - (dispatch [::parse-commands! contact file]) - (dispatch [::fetch-commands! contact]))) - + [{:keys [current-chat-id contacts chats]} [jail-id callback]] + (let [identity (or jail-id current-chat-id) + contact-ids (if (get contacts identity) + [identity] + (->> (get-in chats [identity :contacts]) + (map :identity) + (into [])))] + (when (seq contacts) + (doseq [contact-id contact-ids] + (when-let [contact (get contacts contact-id)] + (dispatch [::fetch-commands! {:contact contact + :callback callback}])))))) (defn http-get-commands [params url] (http-get url @@ -48,14 +45,18 @@ (defn fetch-commands! - [_ [{{:keys [dapp? dapp-url bot-url whisper-identity]} :contact - :as params}]] - (if - bot-url + [_ [{{:keys [whisper-identity + dapp-url + bot-url + dapp?]} :contact + :as params}]] + (if bot-url (if-let [resource (js-res/get-resource bot-url)] (dispatch [::validate-hash params resource]) (http-get-commands params bot-url)) - (dispatch [::validate-hash params js-res/commands-js]))) + (when-not dapp? + ;; TODO: this part should be removed in the future + (dispatch [::validate-hash params js-res/wallet-js])))) (defn dispatch-loaded! [db [{{:keys [whisper-identity]} :contact @@ -98,21 +99,25 @@ (get-hash-by-file file))] (assoc db ::valid-hash valid?))) -(defn mark-as [as coll] +(defn each-merge [coll with] (->> coll - (map (fn [[k v]] [k (assoc v :type as)])) + (map (fn [[k v]] [k (merge v with)])) (into {}))) -(defn filter-forbidden-names [account id commands] +(defn filter-commands [account {:keys [contacts chat-id] :as chat} commands] (->> commands (remove (fn [[_ {:keys [registered-only name]}]] (and (not (:address account)) (not= name "global") registered-only))) - (remove (fn [[n]] - (and - (not= console-chat-id id) - (h/matches (name n) "password")))) + ;; TODO: this part should be removed because it's much better to provide the ability to do this in the API + (map (fn [[k {:keys [name] :as v}]] + [k (assoc v :hidden? (and (some #{name} ["send" "request"]) + (= chat-id wallet-chat-id)))])) + (remove (fn [[k _]] + (and (= (count contacts) 1) + (not= console-chat-id (get (first contacts) :identity)) + (h/matches (name k) "password")))) (into {}))) (defn get-mailmans-commands [db] @@ -129,20 +134,24 @@ (into {}))) (defn add-commands - [db [id _ {:keys [commands responses subscriptions]}]] + [{:keys [chats] :as db} [id _ {:keys [commands responses subscriptions]}]] (let [account @(subscribe [:get-current-account]) - commands' (filter-forbidden-names account id commands) + chat (get chats id) + commands' (filter-commands account chat commands) + responses' (filter-commands account chat responses) global-command (:global commands') commands'' (apply dissoc commands' [:init :global]) - responses' (filter-forbidden-names account id responses) mailman-commands (get-mailmans-commands db)] (cond-> db true (update-in [:contacts id] assoc - :commands-loaded true - :commands (mark-as :command (merge mailman-commands commands'')) - :responses (mark-as :response responses') + :commands-loaded? true + :commands (-> (merge mailman-commands commands'') + (each-merge {:type :command + :owner-id id})) + :responses (each-merge responses' {:type :response + :owner-id id}) :subscriptions subscriptions) global-command @@ -167,7 +176,7 @@ [{:keys [global-commands contacts]} [id]] (let [command (get global-commands (keyword id)) commands (get-in contacts [id :commands]) - responses (get-in contacts [id :commands])] + responses (get-in contacts [id :responses])] (contacts/save {:whisper-identity id :global-command command :commands (vals commands) @@ -187,7 +196,7 @@ (reg-handler :check-and-load-commands! (u/side-effect! (fn [{:keys [contacts]} [identity callback]] - (if (get-in contacts [identity :commands-loaded]) + (if (get-in contacts [identity :commands-loaded?]) (callback) (dispatch [:load-commands! identity callback]))))) diff --git a/src/status_im/commands/utils.cljs b/src/status_im/commands/utils.cljs index 05de9df8a0..25df4c3c82 100644 --- a/src/status_im/commands/utils.cljs +++ b/src/status_im/commands/utils.cljs @@ -5,8 +5,10 @@ [status-im.components.react :as components] [status-im.chat.views.input.web-view :as chat-web-view] [status-im.chat.views.input.validation-messages :as chat-validation-messages] + [status-im.chat.views.choosers.choose-contact :as choose-contact] [status-im.components.qr-code :as qr] - [status-im.utils.handlers :refer [register-handler]])) + [status-im.utils.handlers :refer [register-handler]] + [taoensso.timbre :as log])) (defn json->clj [json] (when-not (= json "undefined") @@ -25,7 +27,8 @@ :touchable components/touchable-highlight :activity-indicator components/activity-indicator :bridged-web-view chat-web-view/bridged-web-view - :validation-message chat-validation-messages/validation-message}) + :validation-message chat-validation-messages/validation-message + :choose-contact choose-contact/choose-contact-view}) (defn get-element [n] (elements (keyword (.toLowerCase n)))) diff --git a/src/status_im/contacts/handlers.cljs b/src/status_im/contacts/handlers.cljs index 9d0533759a..3a54d7bda3 100644 --- a/src/status_im/contacts/handlers.cljs +++ b/src/status_im/contacts/handlers.cljs @@ -262,7 +262,7 @@ :dapp-hash dapp-hash}] (dispatch [:add-contacts [contact]]) (when bot-url - (dispatch [:load-commands! contact])))))))))) + (dispatch [:load-commands! id'])))))))))) (register-handler :add-contacts diff --git a/src/status_im/contacts/subs.cljs b/src/status_im/contacts/subs.cljs index e53b298277..55a28a07b8 100644 --- a/src/status_im/contacts/subs.cljs +++ b/src/status_im/contacts/subs.cljs @@ -39,6 +39,11 @@ (let [contacts (subscribe [:all-added-contacts])] (reaction (remove #(true? (:dapp? %)) @contacts))))) +(register-sub :people-in-current-chat + (fn [{:keys [current-chat-id]} _] + (let [contacts (subscribe [:current-chat-contacts])] + (reaction (remove #(true? (:dapp? %)) @contacts))))) + (defn filter-group-contacts [group-contacts contacts] (filter #(group-contacts (:whisper-identity %)) contacts)) diff --git a/src/status_im/data_store/contacts.cljs b/src/status_im/data_store/contacts.cljs index a4665fcc4f..7fd410c7d8 100644 --- a/src/status_im/data_store/contacts.cljs +++ b/src/status_im/data_store/contacts.cljs @@ -6,6 +6,10 @@ [[_ {:keys [name] :as command}]] [(keyword name) command]) +(defn- enrich-with-owner-id [owner-id] + (fn [[k v]] + [k (assoc v :owner-id owner-id)])) + (defn- commands-map->commands-list [commands-map] (or (if (and commands-map (map? commands-map)) @@ -16,9 +20,12 @@ (defn get-all [] (map - (fn [{:keys [commands responses] :as contact}] + (fn [{:keys [commands responses whisper-identity] :as contact}] (assoc contact - :commands (into {} (map command->map-item commands)) + :commands (->> commands + (map command->map-item) + (map (enrich-with-owner-id whisper-identity)) + (into {})) :responses (into {} (map command->map-item responses)))) (data-store/get-all-as-list))) diff --git a/src/status_im/data_store/messages.cljs b/src/status_im/data_store/messages.cljs index 1f360c0599..b7b168354b 100644 --- a/src/status_im/data_store/messages.cljs +++ b/src/status_im/data_store/messages.cljs @@ -7,19 +7,6 @@ [status-im.constants :as c]) (:refer-clojure :exclude [update])) -(defn- map-to-str - [m] - (join ";" (map #(join "=" %) (stringify-keys m)))) - -(defn- str-to-map - [s] - (->> (keywordize-keys (apply hash-map (split s #"[;=]"))) - (map (fn [[k v]] - [k (if (= k :params) - (keywordize-keys (read-string v)) - v)])) - (into {}))) - (defn- user-statuses-to-map [user-statuses] (->> (vals user-statuses) @@ -49,7 +36,7 @@ (defn get-message-content-by-id [message-id] (when-let [{:keys [content-type content] :as message} (get-by-id message-id)] (when (command-type? content-type) - (str-to-map content)))) + (read-string content)))) (defn get-messages [messages] @@ -59,7 +46,7 @@ reverse (keep (fn [{:keys [content-type] :as message}] (if (command-type? content-type) - (clojure.core/update message :content str-to-map) + (clojure.core/update message :content read-string) message))))) (defn get-count-by-chat-id @@ -76,7 +63,7 @@ reverse (keep (fn [{:keys [content-type preview] :as message}] (if (command-type? content-type) - (clojure.core/update message :content str-to-map) + (clojure.core/update message :content read-string) message)))))) (defn get-log-messages @@ -89,7 +76,7 @@ [chat-id] (if-let [{:keys [content-type] :as message} (data-store/get-last-message chat-id)] (if (command-type? content-type) - (clojure.core/update message :content str-to-map) + (clojure.core/update message :content read-string) message))) (defn get-last-outgoing @@ -116,7 +103,7 @@ (when-not (data-store/exists? message-id) (let [content' (if (string? content) content - (map-to-str content)) + (pr-str content)) message' (merge default-values message {:chat-id chat-id diff --git a/src/status_im/data_store/realm/schemas/account/core.cljs b/src/status_im/data_store/realm/schemas/account/core.cljs index ab8f07b70a..928192bcdd 100644 --- a/src/status_im/data_store/realm/schemas/account/core.cljs +++ b/src/status_im/data_store/realm/schemas/account/core.cljs @@ -7,7 +7,8 @@ [status-im.data-store.realm.schemas.account.v6.core :as v6] [status-im.data-store.realm.schemas.account.v7.core :as v7] [status-im.data-store.realm.schemas.account.v8.core :as v8] - [status-im.data-store.realm.schemas.account.v9.core :as v9])) + [status-im.data-store.realm.schemas.account.v9.core :as v9] + [status-im.data-store.realm.schemas.account.v10.core :as v10])) ; put schemas ordered by version (def schemas [{:schema v1/schema @@ -36,4 +37,7 @@ :migration v8/migration} {:schema v9/schema :schemaVersion 9 - :migration v9/migration}]) + :migration v9/migration} + {:schema v10/schema + :schemaVersion 10 + :migration v10/migration}]) diff --git a/src/status_im/data_store/realm/schemas/account/v10/core.cljs b/src/status_im/data_store/realm/schemas/account/v10/core.cljs new file mode 100644 index 0000000000..6478559686 --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/v10/core.cljs @@ -0,0 +1,39 @@ +(ns status-im.data-store.realm.schemas.account.v10.core + (:require [status-im.data-store.realm.schemas.account.v4.chat :as chat] + [status-im.data-store.realm.schemas.account.v1.chat-contact :as chat-contact] + [status-im.data-store.realm.schemas.account.v6.command :as command] + [status-im.data-store.realm.schemas.account.v9.command-parameter :as command-parameter] + [status-im.data-store.realm.schemas.account.v7.contact :as contact] + [status-im.data-store.realm.schemas.account.v1.discover :as discover] + [status-im.data-store.realm.schemas.account.v1.kv-store :as kv-store] + [status-im.data-store.realm.schemas.account.v10.message :as message] + [status-im.data-store.realm.schemas.account.v7.pending-message :as pending-message] + [status-im.data-store.realm.schemas.account.v1.processed-message :as processed-message] + [status-im.data-store.realm.schemas.account.v1.request :as request] + [status-im.data-store.realm.schemas.account.v1.tag :as tag] + [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] + [status-im.data-store.realm.schemas.account.v5.contact-group :as contact-group] + [status-im.data-store.realm.schemas.account.v5.group-contact :as group-contact] + [status-im.data-store.realm.schemas.account.v8.local-storage :as local-storage] + [taoensso.timbre :as log])) + +(def schema [chat/schema + chat-contact/schema + command/schema + command-parameter/schema + contact/schema + discover/schema + kv-store/schema + message/schema + pending-message/schema + processed-message/schema + request/schema + tag/schema + user-status/schema + contact-group/schema + group-contact/schema + local-storage/schema]) + +(defn migration [old-realm new-realm] + (log/debug "migrating v10 account database: " old-realm new-realm) + (message/migration old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/account/v10/message.cljs b/src/status_im/data_store/realm/schemas/account/v10/message.cljs new file mode 100644 index 0000000000..aaad25cbbc --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/v10/message.cljs @@ -0,0 +1,51 @@ +(ns status-im.data-store.realm.schemas.account.v10.message + (:require [taoensso.timbre :as log] + [clojure.string :as str])) + +(def schema {:name :message + :primaryKey :message-id + :properties {:message-id :string + :from :string + :to {:type :string + :optional true} + :group-id {:type :string + :optional true} + :content :string ;; TODO make it ArrayBuffer + :content-type :string + :username {:type :string + :optional true} + :timestamp :int + :chat-id {:type :string + :indexed true} + :outgoing :bool + :retry-count {:type :int + :default 0} + :same-author :bool + :same-direction :bool + :preview {:type :string + :optional true} + :message-type {:type :string + :optional true} + :message-status {:type :string + :optional true} + :user-statuses {:type :list + :objectType "user-status"} + :clock-value {:type :int + :default 0} + :show? {:type :bool + :default true}}}) + +(defn migration [old-realm new-realm] + (log/debug "migrating message schema v10") + (let [messages (.objects new-realm "message")] + (dotimes [i (.-length messages)] + (let [message (aget messages i) + content (aget message "content") + type (aget message "content-type")] + (when (and (or (= type "wallet-command") + (= type "wallet-request") + (= type "command") + (= type "command-request")) + (or (> (str/index-of content "command=send") -1) + (> (str/index-of content "command=request") -1))) + (aset message "show?" false)))))) diff --git a/src/status_im/data_store/realm/schemas/account/v8/core.cljs b/src/status_im/data_store/realm/schemas/account/v8/core.cljs index 42c97788e7..eda0ed4eac 100644 --- a/src/status_im/data_store/realm/schemas/account/v8/core.cljs +++ b/src/status_im/data_store/realm/schemas/account/v8/core.cljs @@ -35,5 +35,4 @@ local-storage/schema]) (defn migration [old-realm new-realm] - (log/debug "migrating v8 account database: " old-realm new-realm) - (contact/migration old-realm new-realm)) + (log/debug "migrating v8 account database: " old-realm new-realm)) diff --git a/src/status_im/utils/js_resources.cljs b/src/status_im/utils/js_resources.cljs index a08574884c..a3d6b1093b 100644 --- a/src/status_im/utils/js_resources.cljs +++ b/src/status_im/utils/js_resources.cljs @@ -21,8 +21,6 @@ (def demo-bot-js (slurp-bot :demo_bot)) -(def commands-js wallet-js) - (def resources {:wallet-bot wallet-js :console-bot console-js