Cljs commands proposal
This commit is contained in:
parent
e817b3f593
commit
b6a68e79c7
|
@ -0,0 +1,107 @@
|
|||
(ns status-im.chat.commands.core
|
||||
(:require [clojure.set :as set]
|
||||
[status-im.chat.commands.protocol :as protocol]
|
||||
[status-im.chat.commands.impl.transactions :as transactions]
|
||||
[status-im.chat.models.input :as input-model]))
|
||||
|
||||
(def ^:private arg-wrapping-char "\"")
|
||||
(def ^:private command-char "/")
|
||||
(def ^:private space-char " ")
|
||||
|
||||
(def commands-register
|
||||
"Register of all commands. Whenever implementing a new command,
|
||||
provide the implementation in the `status-im.chat.commands.impl.*` ns,
|
||||
and add its instance here."
|
||||
#{(transactions/PersonalSendCommand.)})
|
||||
|
||||
(defn validate-and-send
|
||||
"Validates and sends command in current chat"
|
||||
[command cofx]
|
||||
nil)
|
||||
|
||||
(defn send
|
||||
"Sends command with given arguments in particular chat"
|
||||
[command chat-id cofx]
|
||||
nil)
|
||||
|
||||
(def command-id (juxt protocol/id protocol/scope))
|
||||
|
||||
(defn- prepare-params
|
||||
"Prepares parameters sequence of command by providing suggestion components with
|
||||
selected-event injected with correct arg indexes and `last-arg?` flag."
|
||||
[command]
|
||||
(let [parameters (protocol/parameters command)
|
||||
last-param-idx (dec (count parameters))]
|
||||
(into []
|
||||
(map-indexed (fn [idx {:keys [suggestions] :as param}]
|
||||
(if suggestions
|
||||
(update param :suggestions partial
|
||||
(fn [value]
|
||||
[:set-command-parameter
|
||||
(= idx last-param-idx) idx value]))
|
||||
param))
|
||||
parameters))))
|
||||
|
||||
(defn- add-exclusive-choices [initial-scope exclusive-choices]
|
||||
(reduce (fn [scopes-set exclusive-choices]
|
||||
(reduce (fn [scopes-set scope]
|
||||
(let [exclusive-match (set/intersection scope exclusive-choices)]
|
||||
(if (seq exclusive-match)
|
||||
(reduce conj
|
||||
(disj scopes-set scope)
|
||||
(map (partial conj
|
||||
(set/difference scope exclusive-match))
|
||||
exclusive-match))
|
||||
scopes-set)))
|
||||
scopes-set
|
||||
scopes-set))
|
||||
#{initial-scope}
|
||||
exclusive-choices))
|
||||
|
||||
(defn index-commands
|
||||
"Takes collecton of things implementing the command protocol, and
|
||||
correctly indexes them by their composite ids and access scopes."
|
||||
[commands {:keys [db]}]
|
||||
(let [id->command (reduce (fn [acc command]
|
||||
(assoc acc (command-id command)
|
||||
{:type command
|
||||
:params (prepare-params command)}))
|
||||
{}
|
||||
commands)
|
||||
access-scope->command-id (reduce-kv (fn [acc command-id {:keys [type]}]
|
||||
(let [access-scopes (add-exclusive-choices
|
||||
(protocol/scope type)
|
||||
protocol/or-scopes)]
|
||||
(reduce (fn [acc access-scope]
|
||||
(assoc acc
|
||||
access-scope
|
||||
command-id))
|
||||
acc
|
||||
access-scopes)))
|
||||
{}
|
||||
id->command)]
|
||||
{:db (assoc db
|
||||
:id->command id->command
|
||||
:access-scope->command-id access-scope->command-id)}))
|
||||
|
||||
(defn set-command-parameter
|
||||
"Set value as command parameter for the current chat"
|
||||
[last-param? param-index value {:keys [db]}]
|
||||
(let [{:keys [current-chat-id]} db
|
||||
[command & params] (-> (get-in db [:chats current-chat-id :input-text])
|
||||
input-model/split-command-args)
|
||||
param-count (count params)
|
||||
;; put the new value at the right place in parameters array
|
||||
new-params (cond-> (into [] params)
|
||||
(< param-index param-count) (assoc param-index value)
|
||||
(>= param-index param-count) (conj value))
|
||||
;; if the parameter is not the last one for the command, add space
|
||||
input-text (cond-> (str command space-char
|
||||
(input-model/join-command-args
|
||||
new-params))
|
||||
(and (not last-param?)
|
||||
(or (= 0 param-count)
|
||||
(= param-index (dec param-count))))
|
||||
(str space-char))]
|
||||
{:db (assoc-in db [:chats current-chat-id :input-text]
|
||||
(input-model/text->emoji input-text))}))
|
|
@ -0,0 +1,133 @@
|
|||
(ns status-im.chat.commands.impl.transactions
|
||||
(:require-macros [status-im.utils.views :refer [defview letsubs]])
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[status-im.chat.commands.protocol :as protocol]
|
||||
[status-im.ui.components.react :as react]
|
||||
[status-im.ui.components.icons.vector-icons :as vector-icons]
|
||||
[status-im.ui.components.colors :as colors]
|
||||
[status-im.ui.components.list.views :as list]
|
||||
[status-im.i18n :as i18n]
|
||||
[status-im.chat.commands.impl.transactions.styles :as transactions-styles]
|
||||
[status-im.chat.styles.message.message :as message-styles]))
|
||||
|
||||
(defn- render-asset [selected-event-creator]
|
||||
(fn [{:keys [name symbol amount decimals] :as asset}]
|
||||
[react/touchable-highlight
|
||||
{:on-press #(re-frame/dispatch (selected-event-creator symbol))}
|
||||
[react/view transactions-styles/asset-container
|
||||
[react/view transactions-styles/asset-main
|
||||
[react/image {:source (-> asset :icon :source)
|
||||
:style transactions-styles/asset-icon}]
|
||||
[react/text {:style transactions-styles/asset-symbol} symbol]
|
||||
[react/text {:style transactions-styles/asset-name} name]]
|
||||
;;TODO(goranjovic) : temporarily disabled to fix https://github.com/status-im/status-react/issues/4963
|
||||
;;until the resolution of https://github.com/status-im/status-react/issues/4972
|
||||
#_[react/text {:style transactions-styles/asset-balance}
|
||||
(str (money/internal->formatted amount symbol decimals))]]]))
|
||||
|
||||
(def assets-separator [react/view transactions-styles/asset-separator])
|
||||
|
||||
(defview choose-asset [selected-event-creator]
|
||||
(letsubs [assets [:wallet/visible-assets-with-amount]]
|
||||
[react/view
|
||||
[list/flat-list {:data (filter #(not (:nft? %)) assets)
|
||||
:key-fn (comp name :symbol)
|
||||
:render-fn (render-asset selected-event-creator)
|
||||
:enableEmptySections true
|
||||
:separator assets-separator
|
||||
:keyboardShouldPersistTaps :always
|
||||
:bounces false}]]))
|
||||
|
||||
(defn send-short-preview
|
||||
[{:keys [content]}]
|
||||
(let [parameters (:params content)]
|
||||
[react/text {}
|
||||
(str (i18n/label :command-sending)
|
||||
(i18n/label-number (:amount parameters))
|
||||
" "
|
||||
(:asset parameters))]))
|
||||
|
||||
(defview send-status [tx-hash outgoing]
|
||||
(letsubs [confirmed? [:transaction-confirmed? tx-hash]
|
||||
tx-exists? [:wallet-transaction-exists? tx-hash]]
|
||||
[react/touchable-highlight {:on-press #(when tx-exists?
|
||||
(re-frame/dispatch [:show-transaction-details tx-hash]))}
|
||||
[react/view message-styles/command-send-status-container
|
||||
[vector-icons/icon (if confirmed? :icons/check :icons/dots)
|
||||
{:color colors/blue
|
||||
:container-style (message-styles/command-send-status-icon outgoing)}]
|
||||
[react/view
|
||||
[react/text {:style message-styles/command-send-status-text}
|
||||
(i18n/label (cond
|
||||
confirmed? :status-confirmed
|
||||
tx-exists? :status-pending
|
||||
:else :status-tx-not-found))]]]]))
|
||||
|
||||
(defview send-preview
|
||||
[{:keys [content timestamp-str outgoing group-chat]}]
|
||||
(letsubs [network [:network-name]]
|
||||
(let [{{:keys [amount fiat-amount tx-hash asset currency] send-network :network} :params} content
|
||||
recipient-name (get-in content [:params :bot-db :public :recipient])
|
||||
network-mismatch? (and (seq send-network) (not= network send-network))]
|
||||
[react/view message-styles/command-send-message-view
|
||||
[react/view
|
||||
[react/view message-styles/command-send-amount-row
|
||||
[react/view message-styles/command-send-amount
|
||||
[react/text {:style message-styles/command-send-amount-text
|
||||
:font :medium}
|
||||
amount
|
||||
[react/text {:style (message-styles/command-amount-currency-separator outgoing)}
|
||||
"."]
|
||||
[react/text {:style (message-styles/command-send-currency-text outgoing)
|
||||
:font :default}
|
||||
asset]]]]
|
||||
(when fiat-amount
|
||||
[react/view message-styles/command-send-fiat-amount
|
||||
[react/text {:style message-styles/command-send-fiat-amount-text}
|
||||
(str "~ " fiat-amount " " (or currency (i18n/label :usd-currency)))]])
|
||||
(when (and group-chat
|
||||
recipient-name)
|
||||
[react/text {:style message-styles/command-send-recipient-text}
|
||||
(str
|
||||
(i18n/label :send-sending-to)
|
||||
" "
|
||||
recipient-name)])
|
||||
[react/view
|
||||
[react/text {:style (message-styles/command-send-timestamp outgoing)}
|
||||
(str (i18n/label :sent-at) " " timestamp-str)]]
|
||||
[send-status tx-hash outgoing]
|
||||
(when network-mismatch?
|
||||
[react/text send-network])]])))
|
||||
|
||||
(deftype PersonalSendCommand []
|
||||
protocol/Command
|
||||
(id [_]
|
||||
:send)
|
||||
(scope [_]
|
||||
#{:personal-chats})
|
||||
(parameters [_]
|
||||
[{:id :asset
|
||||
:type :text
|
||||
:placeholder "Currency"
|
||||
;; Suggestion components should be structured in such way that they will just take
|
||||
;; one argument, event-creator fn used to construct event to fire whenever something
|
||||
;; is selected.
|
||||
:suggestions choose-asset}
|
||||
{:id :amount
|
||||
:type :number
|
||||
:placeholder "Amount"}])
|
||||
(validate [_ _ _]
|
||||
;; There is no validation for the `/send` command, as it's fully delegated to the wallet
|
||||
nil)
|
||||
(yield-control [_ parameters cofx]
|
||||
;; navigate to wallet
|
||||
nil)
|
||||
(on-send [_ message-id parameters cofx]
|
||||
(when-let [tx-hash (get-in cofx [:db :wallet :send-transaction :tx-hash])]
|
||||
{:dispatch [:update-transactions]}))
|
||||
(on-receive [_ _ _]
|
||||
nil)
|
||||
(short-preview [_ command-message _]
|
||||
(send-short-preview command-message))
|
||||
(preview [_ command-message _]
|
||||
(send-preview command-message)))
|
|
@ -0,0 +1,35 @@
|
|||
(ns status-im.chat.commands.impl.transactions.styles
|
||||
(:require [status-im.ui.components.colors :as colors]))
|
||||
|
||||
(def asset-container
|
||||
{:flex-direction :row
|
||||
:align-items :center
|
||||
:justify-content :space-between
|
||||
:padding-vertical 11})
|
||||
|
||||
(def asset-main
|
||||
{:flex 1
|
||||
:flex-direction :row
|
||||
:align-items :center})
|
||||
|
||||
(def asset-icon
|
||||
{:width 30
|
||||
:height 30
|
||||
:margin-left 14
|
||||
:margin-right 12})
|
||||
|
||||
(def asset-symbol
|
||||
{:color colors/black})
|
||||
|
||||
(def asset-name
|
||||
{:color colors/gray
|
||||
:padding-left 4})
|
||||
|
||||
(def asset-balance
|
||||
{:color colors/gray
|
||||
:padding-right 14})
|
||||
|
||||
(def asset-separator
|
||||
{:height 1
|
||||
:background-color colors/gray-light
|
||||
:margin-left 56})
|
|
@ -0,0 +1,48 @@
|
|||
(ns status-im.chat.commands.protocol)
|
||||
|
||||
(def or-scopes
|
||||
"Scope contexts representing OR choices"
|
||||
[#{:personal-chats :group-chats :public-chats}])
|
||||
|
||||
(defprotocol Command
|
||||
"Protocol for defining command message behaviour"
|
||||
(id [this] "Identifier of the command, used to look-up command display name as well")
|
||||
(scope [this]
|
||||
"Scope of the command, defined as set of values representing contexts
|
||||
where in which command is available, together with `id` it forms unique
|
||||
identifier for each command.
|
||||
Available values for the set are:
|
||||
`id-of-the-any-chat` - command if available only for the specified chat
|
||||
`:personal-chats` - command is available for any personal 1-1 chat
|
||||
`:group-chats` - command is available for any group chat
|
||||
`:public-chats` - command is available for any public chat
|
||||
`:requested` - command is available only when there is an outstanding request")
|
||||
(parameters [this]
|
||||
"Ordered sequence of command parameter templates, where each parameter
|
||||
is defined as map consisting of mandatory `:id`, `:title` and `:type` keys,
|
||||
and optional `:suggestions` field.
|
||||
When used, `:suggestions` containes reference to any generic helper component
|
||||
rendering suggestions for the argument (input code will handle when and where
|
||||
to render it)")
|
||||
(validate [this parameters cofx]
|
||||
"Function validating the parameters once command is send. Takes parameters map
|
||||
and `cofx` map as argument, returns either `nil` meaning that no errors were
|
||||
found and command send workflow can proceed, or sequence of errors to display")
|
||||
(yield-control [this parameters cofx]
|
||||
"Optional function, which if implemented, can step out of the normal command
|
||||
workflow (`validate-and-send`) and yield control back to application before sending.
|
||||
Useful for cases where we want to use command input handling (parameters) and/or
|
||||
validating, but we don't want to send message before yielding control elsewhere.")
|
||||
(on-send [this message-id parameters cofx]
|
||||
"Function which can provide any extra effects to be produced in addition to
|
||||
normal message effects which happen whenever message is sent")
|
||||
(on-receive [this command-message cofx]
|
||||
"Function which can provide any extre effects to be produced in addition to
|
||||
normal message effects which happen when particular command message is received")
|
||||
(short-preview [this command-message cofx]
|
||||
"Function rendering the short-preview of the command message, used when
|
||||
displaying the last message in list of chats on home tab.
|
||||
There is no argument names `parameters` anymore, as the message object
|
||||
contains everything needed for short-preview/preview to render.")
|
||||
(preview [this command-message cofx]
|
||||
"Function rendering preview of the command message in message stream"))
|
|
@ -14,8 +14,8 @@
|
|||
status-im.ui.screens.group.events
|
||||
[status-im.ui.screens.navigation :as navigation]
|
||||
[status-im.utils.universal-links.core :as universal-links]
|
||||
|
||||
status-im.utils.universal-links.events
|
||||
[status-im.chat.commands.core :as commands]
|
||||
status-im.ui.screens.add-new.new-chat.navigation
|
||||
status-im.ui.screens.network-settings.events
|
||||
status-im.ui.screens.profile.events
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
(ns status-im.test.chat.commands.core
|
||||
(:require [cljs.test :refer-macros [deftest is testing]]
|
||||
[status-im.chat.commands.core :as core]
|
||||
[status-im.chat.commands.protocol :as protocol]))
|
||||
|
||||
(defn- fake-suggestion
|
||||
[selected-event-creator value]
|
||||
(selected-event-creator value))
|
||||
|
||||
(deftype TestCommand []
|
||||
protocol/Command
|
||||
(id [_]
|
||||
:test-command)
|
||||
(scope [_]
|
||||
#{:personal-chats :group-chats :public-chats :requested})
|
||||
(parameters [_]
|
||||
[{:id :first-param
|
||||
:type :text
|
||||
;; pass function as mock-up for suggestions component, so we can
|
||||
;; just test the correct injection of `:set-command-parameter` event
|
||||
:suggestions fake-suggestion}
|
||||
{:id :second-param
|
||||
:type :text}
|
||||
{:id :last-param
|
||||
:type :text
|
||||
:suggestions fake-suggestion}])
|
||||
(validate [_ parameters _]
|
||||
(when-not (every? (comp string? second) parameters)
|
||||
"Not all parameters are filled and of the correc type"))
|
||||
(yield-control [_ _ _]
|
||||
nil)
|
||||
(on-send [_ _ _ _]
|
||||
nil)
|
||||
(on-receive [_ _ _]
|
||||
nil)
|
||||
(short-preview [_ command-message _]
|
||||
[:text (str "Test-command, first-param: "
|
||||
(get-in command-message [:content :params :first-param]))])
|
||||
(preview [_ command-message _]
|
||||
[:text (str "Test-command, params: "
|
||||
(apply str (map [:first-param :second-param :last-param]
|
||||
(get-in command-message [:content :params]))))]))
|
||||
|
||||
(def TestCommandInstance (TestCommand.))
|
||||
|
||||
(deftest index-commands-test
|
||||
(let [fx (core/index-commands #{TestCommandInstance} {:db {}})]
|
||||
(testing "Primary composite key index for command is correctly created"
|
||||
(is (= TestCommandInstance
|
||||
(get-in fx [:db :id->command
|
||||
(core/command-id TestCommandInstance) :type]))))
|
||||
(testing "Suggestions for parameters are injected with correct selection events"
|
||||
(is (= [:set-command-parameter false 0 "first-value"]
|
||||
((get-in fx [:db :id->command
|
||||
(core/command-id TestCommandInstance) :params
|
||||
0 :suggestions])
|
||||
"first-value")))
|
||||
(is (= [:set-command-parameter true 2 "last-value"]
|
||||
((get-in fx [:db :id->command
|
||||
(core/command-id TestCommandInstance) :params
|
||||
2 :suggestions])
|
||||
"last-value"))))
|
||||
(testing "Access scope indexes are correctly created"
|
||||
(is (= (get-in fx [:db :access-scope->command-id #{:personal-chats :requested}])
|
||||
(core/command-id TestCommandInstance)))
|
||||
(is (= (get-in fx [:db :access-scope->command-id #{:group-chats :requested}])
|
||||
(core/command-id TestCommandInstance)))
|
||||
(is (= (get-in fx [:db :access-scope->command-id #{:public-chats :requested}])
|
||||
(core/command-id TestCommandInstance))))))
|
||||
|
||||
(deftest set-command-parameter-test
|
||||
(testing "Setting command parameter correctly updates the text input"
|
||||
(let [create-cofx (fn [input-text]
|
||||
{:db {:chats {"test" {:input-text input-text}}
|
||||
:current-chat-id "test"}})]
|
||||
(is (= "/test-command first-value "
|
||||
(get-in (core/set-command-parameter
|
||||
false 0 "first-value"
|
||||
(create-cofx "/test-command"))
|
||||
[:db :chats "test" :input-text])))
|
||||
(is (= "/test-command first-value second-value \"last value\""
|
||||
(get-in (core/set-command-parameter
|
||||
false 1 "second-value"
|
||||
(create-cofx "/test-command first-value edited \"last value\""))
|
||||
[:db :chats "test" :input-text])))
|
||||
(is (= "/test-command first-value second-value \"last value\""
|
||||
(get-in (core/set-command-parameter
|
||||
false 2 "last value"
|
||||
(create-cofx "/test-command first-value second-value"))
|
||||
[:db :chats "test" :input-text]))))))
|
|
@ -26,6 +26,7 @@
|
|||
[status-im.test.chat.subs]
|
||||
[status-im.test.chat.views.message]
|
||||
[status-im.test.chat.views.photos]
|
||||
[status-im.test.chat.commands.core]
|
||||
[status-im.test.i18n]
|
||||
[status-im.test.protocol.web3.inbox]
|
||||
[status-im.test.utils.utils]
|
||||
|
@ -81,6 +82,7 @@
|
|||
'status-im.test.chat.models.message
|
||||
'status-im.test.chat.views.message
|
||||
'status-im.test.chat.views.photos
|
||||
'status-im.test.chat.commands.core
|
||||
'status-im.test.i18n
|
||||
'status-im.test.transport.core
|
||||
'status-im.test.transport.inbox
|
||||
|
|
Loading…
Reference in New Issue