/send and /request commands; commands in group chats and fixes for parameter and result boxes

This commit is contained in:
alwx 2017-05-10 19:42:52 +03:00 committed by Roman Volosovskyi
parent a2b2851061
commit 5c15df9b64
46 changed files with 1143 additions and 378 deletions

View File

@ -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"])
}

View File

@ -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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

View File

@ -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(

View File

@ -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();
}

View File

@ -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',

View File

@ -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 () {}
}

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

View File

@ -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
},

View File

@ -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))))

View File

@ -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)

View File

@ -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})))))))

View File

@ -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]))))))

View File

@ -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)

View File

@ -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

View File

@ -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))))

View File

@ -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 {})))

View File

@ -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})

View File

@ -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})

View File

@ -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]]

View File

@ -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}]])

View File

@ -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]]]]])))))

View File

@ -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}])))

View File

@ -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]]))

View File

@ -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]]))

View File

@ -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}]])}]))

View File

@ -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

View File

@ -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?)

View File

@ -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]

View File

@ -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])))))

View File

@ -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))))

View File

@ -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

View File

@ -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))

View File

@ -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)))

View File

@ -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

View File

@ -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}])

View File

@ -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))

View File

@ -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))))))

View File

@ -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))

View File

@ -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