interactive suggestions

This commit is contained in:
Roman Volosovskyi 2017-03-23 18:52:38 +02:00 committed by Roman Volosovskyi
parent deb7b2b617
commit 03cab7ace2
23 changed files with 247 additions and 68 deletions

View File

@ -329,9 +329,10 @@ function jsSuggestions(params, context) {
])]);
if (suggestion.pressValue) {
suggestionMarkup = status.components.touchable({
onPress: [status.events.SET_VALUE, suggestion.pressValue]
onPress: status.components.dispatch([status.events.SET_VALUE, suggestion.pressValue])
},
suggestionMarkup);
suggestionMarkup
);
}
sugestionsMarkup.push(suggestionMarkup);
}
@ -422,7 +423,7 @@ function phoneSuggestions(params, context) {
suggestions = ph.map(function (phone) {
return status.components.touchable(
{onPress: [status.events.SET_COMMAND_ARGUMENT, [0, phone.number]]},
{onPress: status.components.dispatch([status.events.SET_VALUE, phone.number])},
status.components.view(suggestionContainerStyle,
[status.components.view(suggestionSubContainerStyle,
[
@ -484,7 +485,7 @@ var faucets = [
function faucetSuggestions(params) {
var suggestions = faucets.map(function (entry) {
return status.components.touchable(
{onPress: [status.events.SET_COMMAND_ARGUMENT, [0, entry.url]]},
{onPress: status.components.dispatch([status.events.SET_COMMAND_ARGUMENT, [0, entry.url]])},
status.components.view(
suggestionContainerStyle,
[status.components.view(
@ -559,7 +560,7 @@ status.command({
function debugSuggestions(params) {
var suggestions = ["On", "Off"].map(function (entry) {
return status.components.touchable(
{onPress: [status.events.SET_COMMAND_ARGUMENT, [0, entry]]},
{onPress: status.components.dispatch([status.events.SET_VALUE, entry])},
status.components.view(
suggestionContainerStyle,
[status.components.view(

View File

@ -26,7 +26,7 @@ status.defineSubscription(
function (params) {
return round(params.value);
}
)
);
function superSuggestion(params, context) {
var balance = parseFloat(web3.fromWei(web3.eth.getBalance(context.from), "ether"));
@ -69,11 +69,11 @@ function superSuggestion(params, context) {
validationText: validationText
});
status.setSuggestions(view);
return {markup: view};
};
status.on("text-change", superSuggestion);
status.on("message", function (params, context) {
status.addListener("on-message-input-change", superSuggestion);
status.addListener("on-message-send", function (params, context) {
if (isNaN(params.message)) {
status.sendMessage("Seems that you don't want to send money :(");
return;

View File

@ -47,7 +47,3 @@ status.command({
type: status.types.TEXT,
placeholder: I18n.t('location_address')
});
status.addListener("init", function (params, context) {
return {"text-message": "Hello, man!"};
});

View File

@ -32,6 +32,16 @@
"bot-url": "local://mailman-bot"
},
"demo-bot":
{
"name":
{
"en": "Demo bot"
},
"dapp?": true,
"bot-url": "local://demo-bot"
},
"browse":
{
"name":

View File

@ -1,7 +1,8 @@
var _status_catalog = {
commands: {},
responses: {},
functions: {}
functions: {},
subscriptions: {}
},
status = {};
@ -109,6 +110,10 @@ function view(options, elements) {
return ['view', options].concat(elements);
}
function slider(options) {
return ['slider', options];
}
function image(options) {
return ['image', options];
}
@ -121,6 +126,14 @@ function scrollView(options, elements) {
return ['scroll-view', options].concat(elements);
}
function subscribe(path) {
return ['subscribe', path];
}
function dispatch(path) {
return ['dispatch', path];
}
function webView(url) {
return ['web-view', {
source: {
@ -178,16 +191,25 @@ var status = {
components: {
view: view,
text: text,
slider: slider,
image: image,
touchable: touchable,
scrollView: scrollView,
webView: webView,
validationMessage: validationMessage,
bridgedWebView: bridgedWebView
bridgedWebView: bridgedWebView,
subscribe: subscribe,
dispatch: dispatch
},
setSuggestions: function (view) {
addContext("suggestions", view);
},
setDefaultDb: function (db) {
addContext("default-db", db);
},
updateDb: function (db) {
addContext("update-db", db)
},
sendMessage: function (text) {
addContext("text-message", text);
},
@ -202,9 +224,25 @@ var status = {
}
logMessages.push(message);
addContext("log-messages", logMessages);
},
defineSubscription: function (name, subscriptions, handler) {
_status_catalog.subscriptions[name] = {
subscriptions: subscriptions,
handler: handler
};
}
};
function calculateSubscription(parameters, context) {
var subscriptionConfig = _status_catalog.subscriptions[parameters.name];
if (!subscriptionConfig) {
return;
}
return subscriptionConfig.handler(parameters.subscriptions);
}
status.addListener("subscription", calculateSubscription);
console = (function (old) {
return {

View File

@ -0,0 +1,65 @@
(ns status-im.bots.handlers
(:require [re-frame.core :as re-frame]
[status-im.components.status :as status]
[status-im.utils.handlers :as u]))
(defn check-subscriptions
[{:keys [bot-db] :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)]
(doseq [{:keys [bot subscriptions name]} subscriptions]
(let [subs-values (reduce (fn [res [sub-name sub-path]]
(assoc res sub-name (get-in current-bot-db sub-path)))
{} subscriptions)]
(status/call-function!
{:chat-id bot
: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)))
(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)))
(u/register-handler
:register-bot-subscription
(fn [db [_ {:keys [bot subscriptions] :as opts}]]
(reduce
(fn [db [sub-name sub-path]]
(let [sub-path' (if (coll? sub-path) sub-path [sub-path])
sub-path'' (mapv keyword sub-path')]
(update-in db [:bot-subscriptions sub-path''] conj
(assoc-in opts [:subscriptions sub-name] sub-path''))))
db
subscriptions)))
(u/register-handler
::calculated-subscription
(u/side-effect!
(fn [_ [_ {:keys [bot path]
{:keys [error result]} :result
:as data}]]
(when-not error
(let [returned (:returned result)
opts {:bot bot
:path path
:value returned}]
(re-frame/dispatch [:set-in-bot-db opts]))))))
(u/register-handler
:update-bot-db
(fn [app-db [_ {:keys [bot db]}]]
(update-in app-db [:bot-db bot] merge db)))

View File

@ -0,0 +1,15 @@
(ns status-im.bots.subs
(:require-macros [reagent.ratom :refer [reaction]])
(:require [re-frame.core :as re-frame]))
(re-frame/register-sub
:bot-subscription
(fn [db [_ path]]
(let [chat-id (re-frame/subscribe [:get-current-chat-id])]
(reaction (get-in @db (concat [:bot-db @chat-id] path))))))
(re-frame/register-sub
:current-bot-db
(fn [db]
(let [chat-id (re-frame/subscribe [:get-current-chat-id])]
(reaction (get-in @db [:bot-db @chat-id])))))

View File

@ -101,7 +101,7 @@
suggestions (suggestions/get-command-suggestions db chat-text)
global-commands (suggestions/get-global-command-suggestions db chat-text)
{:keys [dapp?]} (get-in db [:contacts chat-id])]
(when (and dapp? (every? empty? [requests suggestions global-commands]))
(when (and dapp? (every? empty? [requests suggestions]))
(dispatch [::check-dapp-suggestions chat-id chat-text]))
(-> db
(assoc-in [:chats chat-id :request-suggestions] requests)
@ -276,13 +276,14 @@
(handlers/register-handler
::check-dapp-suggestions
(handlers/side-effect!
(fn [db [_ chat-id text]]
(fn [{:keys [current-account-id] :as db} [_ chat-id text]]
(let [data (get-in db [:local-storage chat-id])]
(status/call-function!
{:chat-id chat-id
:function :on-message-input-change
:parameters {:message text}
:context {:data data}})))))
:context {:data data
:from current-account-id}})))))
(handlers/register-handler
:clear-seq-arguments

View File

@ -3,7 +3,6 @@
[re-frame.core :refer [enrich after debug dispatch path]]
[status-im.data-store.messages :as messages]
[status-im.chat.utils :as cu]
[status-im.commands.utils :refer [generate-hiccup]]
[status-im.utils.random :as random]
[status-im.constants :refer [wallet-chat-id
content-type-command

View File

@ -190,22 +190,28 @@
(register-handler ::send-dapp-message
(u/side-effect!
(fn [db [_ chat-id {:keys [content]}]]
(fn [{:keys [current-account-id] :as db} [_ chat-id {:keys [content]}]]
(let [data (get-in db [:local-storage chat-id])]
(status/call-function!
{:chat-id chat-id
:function :on-message-send
:parameters {:message content}
:context {:data data}})))))
:context {:data data
:from current-account-id}})))))
(register-handler :received-bot-response
(u/side-effect!
(fn [_ [_ {:keys [chat-id] :as params} {:keys [result] :as data}]]
(let [{:keys [returned context]} result
{:keys [markup text-message]} returned
{:keys [log-messages]} context]
{:keys [log-messages update-db default-db]} context]
(when update-db
(dispatch [:update-bot-db {:bot chat-id
:db update-db}]))
(when markup
(dispatch [:suggestions-handler (assoc params :result data)]))
(dispatch [:suggestions-handler (assoc params
:result data
:default-db default-db)]))
(doseq [message log-messages]
(let [{:keys [message type]} message]
(when (or (not= type "debug") js/goog.DEBUG)

View File

@ -8,14 +8,16 @@
icon]]
[status-im.chat.views.input.animations.expandable :refer [expandable-view]]
[status-im.chat.views.input.utils :as input-utils]
[status-im.commands.utils :as command-utils]
[status-im.i18n :refer [label]]
[taoensso.timbre :as log]
[clojure.string :as str]))
(defview parameter-box-container []
[parameter-box [:chat-parameter-box]]
[parameter-box [:chat-parameter-box]
bot-db [:current-bot-db]]
(when (:hiccup parameter-box)
(:hiccup parameter-box)))
(command-utils/generate-hiccup (:hiccup parameter-box) bot-db)))
(defview parameter-box-view []
[show-parameter-box? [:show-parameter-box?]]

View File

@ -35,6 +35,37 @@
:else nil)))
(defn suggestions-handler!
[{:keys [contacts chats] :as db} [{:keys [chat-id default-db command parameter-index result]}]]
(let [{:keys [markup]} (get-in result [:result :returned])
{:keys [dapp? dapp-url]} (get contacts chat-id)
path (if command
[:chats chat-id :parameter-boxes (:name command) parameter-index :hiccup]
[:chats chat-id :parameter-boxes :message :hiccup])]
(when-not (= (get-in db path) markup)
(dispatch [:set-in path markup])
(when default-db
(dispatch [:update-bot-db {:bot chat-id
:db default-db}])))))
(defn suggestions-events-handler!
[{:keys [current-chat-id bot-db] :as db} [[n & data :as ev] val]]
(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-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?
nil)))
(defn print-error-message! [message]
(fn [_ params]
(when (:error (last params))
@ -48,25 +79,11 @@
(reg-handler
:suggestions-handler
[(after (print-error-message! "Error on param suggestions"))]
(fn [{:keys [contacts chats] :as db} [{:keys [chat-id command parameter-index result]}]]
(let [{:keys [markup]} (get-in result [:result :returned])
{:keys [dapp? dapp-url]} (get contacts chat-id)
hiccup (generate-hiccup markup)
path (if command
[:chats chat-id :parameter-boxes (:name command) parameter-index]
[:chats chat-id :parameter-boxes :message])]
(assoc-in db path (when hiccup
{:hiccup hiccup})))))
(handlers/side-effect! suggestions-handler!))
(reg-handler
:suggestions-event!
(handlers/side-effect!
(fn [{:keys [current-chat-id] :as db} [[n arg]]]
(let [{:keys [dapp?]} (get-in db [:contacts current-chat-id])]
(case (keyword n)
:set-command-argument (dispatch [:set-command-argument arg])
:set-value (dispatch [:set-chat-input-text arg])
nil)))))
(handlers/side-effect! suggestions-events-handler!))
(reg-handler :set-local-storage
(fn [{:keys [current-chat-id] :as db} [{:keys [data] :as event}]]

View File

@ -108,7 +108,7 @@
(into {})))
(defn add-commands
[db [id _ {:keys [commands responses]}]]
[db [id _ {:keys [commands responses subscriptions]}]]
(let [account @(subscribe [:get-current-account])
commands' (filter-forbidden-names account id commands)
global-command (:global commands')
@ -121,7 +121,7 @@
:commands-loaded true
:commands (mark-as :command commands'')
:responses (mark-as :response responses')
:global-command global-command)
:subscriptions subscriptions)
global-command
(update :global-commands assoc (keyword id)
@ -173,7 +173,14 @@
;;(after #(dispatch [:update-suggestions]))
(after (fn [_ [id]]
(dispatch [:invoke-commands-loading-callbacks id])
(dispatch [:invoke-chat-loaded-callbacks id])))]
(dispatch [:invoke-chat-loaded-callbacks id])))
(after (fn [{:keys [contacts]} [id]]
(let [subscriptions (get-in contacts [id :subscriptions])]
(doseq [[name opts] subscriptions]
(dispatch [:register-bot-subscription
(assoc opts :bot id
:name name)])))))]
add-commands)
(reg-handler ::loading-failed! (u/side-effect! loading-failed!))

View File

@ -4,6 +4,7 @@
[status-im.components.react :refer [text
scroll-view
view
slider
web-view
image
touchable-highlight]]
@ -20,6 +21,7 @@
(def elements
{:text text
:view view
:slider slider
:scroll-view scroll-view
:web-view web-view
:image image
@ -30,26 +32,36 @@
(defn get-element [n]
(elements (keyword (.toLowerCase n))))
(def events #{:onPress})
(def events #{:onPress :onValueChange :onSlidingComplete})
(defn wrap-event [event]
#(dispatch [:suggestions-event! event]))
(defn wrap-event [[_ event]]
(let [data (gensym)]
#(dispatch [:suggestions-event! (update event 0 keyword) %])))
(defn check-events [m]
(let [ks (set (keys m))
evs (set/intersection ks events)]
(reduce #(update %1 %2 wrap-event) m evs)))
(defn generate-hiccup [markup]
;; todo implement validation
(defn generate-hiccup
([markup]
(generate-hiccup markup {}))
([markup data]
(w/prewalk
(fn [el]
(if (and (vector? el) (string? (first el)))
(cond
(and (vector? el) (= "subscribe" (first el)))
(let [path (mapv keyword (second el))]
(get-in data path))
(and (vector? el) (string? (first el)))
(-> el
(update 0 get-element)
(update 1 check-events))
el))
markup))
:esle el))
markup)))
(defn reg-handler
([name handler] (reg-handler name nil handler))

View File

@ -51,6 +51,7 @@
(def keyboard (.-Keyboard react-native))
(def linking (.-Linking js/ReactNative))
(def slider (get-class "Slider"))
;; Accessor methods for React Components
(defn add-font-style [style-key {:keys [font] :as opts :or {font :default}}]

View File

@ -147,14 +147,14 @@
(.callJail status chat-id (cljs->json path) (cljs->json params') cb))))))
(defn call-function!
[{:keys [chat-id function] :as opts}]
[{:keys [chat-id function callback] :as opts}]
(let [path [:functions function]
params (select-keys opts [:parameters :context])]
(call-jail
chat-id
path
params
#(dispatch [:received-bot-response {:chat-id chat-id} %]))))
(or callback #(dispatch [:received-bot-response {:chat-id chat-id} %])))))
(defn set-soft-input-mode [mode]
(when status

View File

@ -3,7 +3,6 @@
[clojure.string :refer [join split]]
[status-im.utils.random :refer [timestamp]]
[clojure.walk :refer [stringify-keys keywordize-keys]]
[status-im.commands.utils :refer [generate-hiccup]]
[cljs.reader :refer [read-string]]
[status-im.constants :as c])
(:refer-clojure :exclude [update]))

View File

@ -23,6 +23,7 @@
status-im.transactions.handlers
status-im.network.handlers
status-im.debug.handlers
status-im.bots.handlers
[status-im.utils.types :as t]
[status-im.i18n :refer [label]]
[status-im.constants :refer [console-chat-id]]

View File

@ -8,7 +8,8 @@
status-im.contacts.subs
status-im.new-group.subs
status-im.participants.subs
status-im.transactions.subs))
status-im.transactions.subs
status-im.bots.subs))
(register-sub :get
(fn [db [_ k]]

View File

@ -180,8 +180,9 @@
{:keys [hash]} (get transactions id)
pending-message (get transaction-subscribers message-id)]
(when (and pending-message id hash)
(dispatch [::send-pending-message message-id hash])
(dispatch [::remove-transaction id]))))))
(dispatch [::send-pending-message message-id hash]))
;; todo revisit this
(dispatch [::remove-transaction id])))))
(def wrong-password-code "2")
(def discard-code "4")

View File

@ -17,7 +17,9 @@
(def browse-js (slurp-bot :browse))
(def mailman-js (slurp-bot :mailman ))
(def mailman-js (slurp-bot :mailman))
(def demo-bot-js (slurp-bot :demo_bot))
(def commands-js wallet-js)
@ -25,7 +27,8 @@
{:wallet-bot wallet-js
:console-bot console-js
:browse-bot browse-js
:mailman-bot mailman-js})
:mailman-bot mailman-js
:demo-bot demo-bot-js})
(defn get-resource [url]
(let [resource-name (keyword (subs url (count local-protocol)))]

View File

@ -7,5 +7,9 @@
(defmacro slurp-bot [bot-name & files]
(->> (concat files ["translations.js" "bot.js"])
(map #(clojure.core/slurp (s/join "/" ["bots" (name bot-name) %])))
(map (fn [file-name]
(try
(clojure.core/slurp
(s/join "/" ["bots" (name bot-name) file-name]))
(catch Exception _ ""))))
(apply str)))