Refactor command data loading + chat input handling

Also accomplished was removal of redundant preview loading
and command markup is now stored as cljs data in app-db,
only being translated to RN components in subscriptions
This commit is contained in:
Herich 2017-08-07 13:55:40 +02:00 committed by Roman Volosovskyi
parent 7506689fe5
commit e3f27ee5ee
13 changed files with 661 additions and 512 deletions

View File

View File

@ -0,0 +1,122 @@
(ns status-im.chat.events.commands
(:require [cljs.reader :as reader]
[clojure.string :as str]
[re-frame.core :refer [reg-fx reg-cofx inject-cofx dispatch trim-v]]
[taoensso.timbre :as log]
[status-im.data-store.messages :as msg-store]
[status-im.utils.handlers :refer [register-handler-fx]]
[status-im.components.status :as status]
[status-im.chat.constants :as const]
[status-im.commands.utils :as commands-utils]
[status-im.i18n :as i18n]
[status-im.utils.platform :as platform]))
;;;; Helper fns
(defn generate-context
"Generates context for jail call"
[{:keys [current-account-id chats]} 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))
;;;; Coeffects
(reg-cofx
::get-persisted-message
(fn [coeffects _]
(assoc coeffects :get-persisted-message msg-store/get-by-id)))
;;;; Effects
(reg-fx
::update-persisted-message
(fn [message]
(msg-store/update message)))
(reg-fx
:chat-fx/call-jail
(fn [{:keys [callback-events-creator] :as opts}]
(status/call-jail
(-> opts
(dissoc :callback-events-creator)
(assoc :callback
(fn [jail-response]
(doseq [event (callback-events-creator jail-response)]
(dispatch event))))))))
;;;; Handlers
(register-handler-fx
::jail-command-data-response
[trim-v]
(fn [{:keys [db]} [{{:keys [returned]} :result} {:keys [message-id on-requested]} data-type]]
(cond-> {}
returned
(assoc :db (assoc-in db [:message-data data-type message-id] returned))
(and returned
(= :preview data-type))
(assoc ::update-persisted-message {:message-id message-id
:preview (prn-str returned)})
on-requested
(assoc :dispatch (on-requested returned)))))
(register-handler-fx
:request-command-data
[trim-v]
(fn [{:keys [db]}
[{{:keys [command content-command params type]} :content
:keys [chat-id jail-id] :as message}
data-type]]
(let [{:keys [current-account-id chats]
:contacts/keys [contacts]} db
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 (get-in contacts [jail-id :commands-loaded?])
(let [path [(if (= :response (keyword type)) :responses :commands)
(or content-command command)
data-type]
to (get-in contacts [chat-id :address])
jail-params {:parameters params
:context (generate-context db chat-id to)}]
{:chat-fx/call-jail {:jail-id jail-id
:path path
:params jail-params
:callback-events-creator (fn [jail-response]
[[::jail-command-data-response
jail-response message data-type]])}})
{:dispatch-n [[:add-commands-loading-callback jail-id
#(dispatch [:request-command-data message data-type])]
[:load-commands! jail-id]]}))))
(register-handler-fx
:execute-command-immediately
[trim-v]
(fn [_ [{command-name :name :as command}]]
(case (keyword command-name)
:grant-permissions
{:dispatch [:request-permissions
[:read-external-storage]
#(dispatch [:initialize-geth])]}
(log/debug "ignoring command: " command))))
(register-handler-fx
:request-command-preview
[trim-v (inject-cofx ::get-persisted-message)]
(fn [{:keys [db get-persisted-message]} [{:keys [message-id] :as message}]]
(let [previews (get-in db [:message-data :preview])]
(when-not (contains? previews message-id)
(let [{serialized-preview :preview} (get-persisted-message message-id)]
;; if preview is already cached in db, do not request it from jail
;; and write it directly to message-data path
(if serialized-preview
{:db (assoc-in db
[:message-data :preview message-id]
(reader/read-string serialized-preview))}
{:dispatch [:request-command-data message :preview]}))))))

View File

@ -0,0 +1,493 @@
(ns status-im.chat.events.input
(:require [clojure.string :as str]
[re-frame.core :refer [reg-fx reg-cofx inject-cofx dispatch trim-v]]
[taoensso.timbre :as log]
[status-im.chat.constants :as const]
[status-im.chat.utils :as chat-utils]
[status-im.chat.models.input :as input-model]
[status-im.chat.models.suggestions :as suggestions]
[status-im.components.react :as react-comp]
[status-im.components.status :as status]
[status-im.utils.datetime :as time]
[status-im.utils.handlers :refer [register-handler-db register-handler-fx]]
[status-im.utils.random :as random]
[status-im.i18n :as i18n]))
;;;; Coeffects
(reg-cofx
:now
(fn [coeffects _]
(assoc coeffects :now (time/now-ms))))
(reg-cofx
:random-id
(fn [coeffects _]
(assoc coeffects :random-id (random/id))))
;;;; Effects
(reg-fx
::focus-rn-component
(fn [ref]
(try
(.focus ref)
(catch :default e
(log/debug "Cannot focus the reference")))))
(reg-fx
::blur-rn-component
(fn [ref]
(try
(.blur ref)
(catch :default e
(log/debug "Cannot blur the reference")))))
(reg-fx
::dismiss-keyboard
(fn [_]
(react-comp/dismiss-keyboard!)))
(reg-fx
::set-native-props
(fn [{:keys [ref props]}]
(.setNativeProps ref (clj->js props))))
(reg-fx
:chat-fx/call-jail-function
(fn [{:keys [chat-id function callback-events-creator] :as opts}]
(let [path [:functions function]
params (select-keys opts [:parameters :context])]
(status/call-jail
{:jail-id chat-id
:path path
:params params
:callback (fn [jail-response]
(doseq [event (if callback-events-creator
(callback-events-creator jail-response)
[[:received-bot-response
{:chat-id chat-id}
jail-response]])
:when event]
(dispatch event)))}))))
;;;; Handlers
(register-handler-db
:update-input-data
(fn [db]
(input-model/modified-db-after-change db)))
(register-handler-fx
:set-chat-input-text
[trim-v]
(fn [{{:keys [current-chat-id chats chat-ui-props] :as db} :db} [text chat-id]]
(let [chat-id (or chat-id current-chat-id)]
{:db (assoc-in db
[:chats chat-id :input-text]
(input-model/text->emoji text))
:dispatch [:update-suggestions chat-id text]})))
(register-handler-fx
:add-to-chat-input-text
[trim-v]
(fn [{{:keys [chats current-chat-id]} :db} [text-to-add]]
(let [input-text (get-in chats [current-chat-id :input-text])]
{:dispatch [:set-chat-input-text (str input-text text-to-add)]})))
(register-handler-fx
:select-chat-input-command
[trim-v]
(fn [{{:keys [current-chat-id chat-ui-props]} :db}
[{:keys [prefill prefill-bot-db sequential-params name] :as command} metadata prevent-auto-focus?]]
(cond-> {:dispatch-n [[:set-chat-input-text (str (chat-utils/command-name command)
const/spacing-char
(when-not sequential-params
(input-model/join-command-args prefill)))]
[:clear-bot-db]
[:set-chat-input-metadata metadata]
[:set-chat-ui-props {:show-suggestions? false
:result-box nil
:validation-messages nil
:prev-command name}]
[:load-chat-parameter-box command 0]]}
prefill-bot-db
(update :dispatch-n conj [:update-bot-db {:bot current-chat-id
:db prefill-bot-db}])
(not (and sequential-params
prevent-auto-focus?))
(update :dispatch-n conj [:chat-input-focus :input-ref])
sequential-params
(assoc :dispatch-later (cond-> [{:ms 100 :dispatch [:set-chat-seq-arg-input-text
(str/join const/spacing-char prefill)]}]
(not prevent-auto-focus?)
(conj {:ms 100 :dispatch [:chat-input-focus :seq-input-ref]}))))))
(register-handler-db
:set-chat-input-metadata
[trim-v]
(fn [{:keys [current-chat-id] :as db} [data chat-id]]
(let [chat-id (or chat-id current-chat-id)]
(assoc-in db [:chats chat-id :input-metadata] data))))
(register-handler-fx
:set-command-argument
[trim-v]
(fn [{{:keys [current-chat-id] :as db} :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]))
event-to-dispatch (if seq-params?
[:set-chat-seq-arg-input-text arg]
(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)
(conj command-args arg))]
[:set-chat-input-text (str command-name
const/spacing-char
(input-model/join-command-args command-args)
(when (and move-to-next?
(= index (dec (count command-args))))
const/spacing-char))]))]
{:dispatch event-to-dispatch})))
(register-handler-fx
:chat-input-focus
[trim-v]
(fn [{{:keys [current-chat-id chat-ui-props]} :db} [ref]]
(when-let [cmp-ref (get-in chat-ui-props [current-chat-id ref])]
{::focus-rn-component cmp-ref})))
(register-handler-fx
:chat-input-blur
[trim-v]
(fn [{{:keys [current-chat-id chat-ui-props]} :db} [ref]]
(when-let [cmp-ref (get-in chat-ui-props [current-chat-id ref])]
{::blur-rn-component cmp-ref})))
(register-handler-fx
:update-suggestions
[trim-v]
(fn [{{:keys [current-chat-id] :as db} :db} [chat-id text]]
(let [chat-id (or chat-id current-chat-id)
chat-text (str/trim (or text (get-in db [:chats chat-id :input-text]) ""))
requests (->> (suggestions/get-request-suggestions db chat-text)
(remove (fn [{:keys [type]}]
(= type :grant-permissions))))
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/contacts chat-id])
new-db (cond-> db
true (assoc-in [:chats chat-id :request-suggestions] requests)
true (assoc-in [:chats chat-id :command-suggestions] all-commands)
(and dapp?
(str/blank? chat-text))
(assoc-in [:chats chat-id :parameter-boxes :message] nil))
new-event (when (and dapp?
(not (str/blank? chat-text))
(every? empty? [requests commands]))
[::check-dapp-suggestions chat-id chat-text])]
(cond-> {:db new-db}
new-event (assoc :dispatch new-event)))))
(register-handler-fx
:load-chat-parameter-box
[trim-v]
(fn [{{:keys [current-chat-id bot-db current-account-id] :as db} :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 [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
parameter-index
:suggestions]
args (-> (get-in db [:chats current-chat-id :input-text])
(input-model/split-command-args)
(rest))
seq-arg (get-in db [:chats current-chat-id :seq-argument-input-text])
to (get-in db [:contacts/contacts current-chat-id :address])
params {:parameters {:args args
:bot-db bot-db
:seq-arg seq-arg}
:context (merge {:data data
:from current-account-id
:to to}
(input-model/command-dependent-context-params current-chat-id command))}]
{:chat-fx/call-jail {:jail-id (or bot owner-id current-chat-id)
:path path
:params params
:callback-events-creator (fn [jail-response]
[[:received-bot-response
{:chat-id current-chat-id
:command command
:parameter-index parameter-index}
jail-response]])}})))))
(register-handler-fx
::send-message
[trim-v]
(fn [{{:keys [current-public-key current-account-id] :as db} :db} [command chat-id]]
(let [text (get-in db [:chats chat-id :input-text])
data {:message text
:command command
:chat-id chat-id
:identity current-public-key
:address current-account-id}
events [[:set-chat-input-text nil chat-id]
[:set-chat-input-metadata nil chat-id]
[:set-chat-ui-props {:sending-in-progress? false}]]]
{:dispatch-n (if command
(conj events [:check-commands-handlers! data])
(if (str/blank? text)
events
(conj events [:prepare-message data])))})))
(register-handler-fx
:proceed-command
[trim-v]
(fn [_ [{{:keys [bot]} :command :as content} chat-id]]
(let [params {:content content
:chat-id chat-id
:jail-id (or bot chat-id)}
on-send-params (merge params
{:data-type :on-send
:event-after-creator (fn [_ jail-response]
[::send-command jail-response content chat-id])})
after-validation-events [[::request-command-data on-send-params]]
validation-params (merge params
{:data-type :validator
:event-after-creator (fn [_ jail-response]
[::proceed-validation
jail-response
after-validation-events])})]
{:dispatch [::request-command-data validation-params]})))
(register-handler-fx
::proceed-validation
[trim-v]
(fn [_ [{:keys [markup validationHandler parameters]} proceed-events]]
(let [error-events-creator (fn [validator-result]
[[:set-chat-ui-props {:validation-messages validator-result
:sending-in-progress? false}]])
events (cond
markup
(error-events-creator markup)
validationHandler
[[::execute-validation-handler
validationHandler parameters error-events-creator proceed-events]
[:set-chat-ui-props {:sending-in-progress? false}]]
:default
proceed-events)]
{:dispatch-n events})))
(register-handler-fx
::execute-validation-handler
[trim-v]
(fn [_ [validation-handler-name params error-events-creator proceed-events]]
(let [error-events (when-let [validator (input-model/validation-handler validation-handler-name)]
(validator params error-events-creator))]
{:dispatch-n (or error-events proceed-events)})))
(register-handler-fx
::send-command
[trim-v]
(fn [_ [on-send {{:keys [fullscreen bot]} :command :as content} chat-id]]
(if on-send
{:dispatch-n (cond-> [[:set-chat-ui-props {:result-box on-send
:sending-in-progress? false}]]
fullscreen
(conj [:choose-predefined-expandable-height :result-box :max]))
::dismiss-keyboard nil}
{:dispatch [::request-command-data
{:content content
:chat-id chat-id
:jail-id (or bot chat-id)
:data-type :preview
:event-after-creator (fn [command-message _]
[::send-message command-message chat-id])}]})))
(register-handler-fx
::request-command-data
[trim-v (inject-cofx :random-id) (inject-cofx :now)]
(fn [{{:keys [bot-db] :contacts/keys [contacts]} :db
message-id :random-id
current-time :now}
[{{:keys [command
metadata
args]
:as content} :content
:keys [chat-id jail-id data-type event-after-creator]}]]
(let [{:keys [dapp? dapp-url name]} (get contacts chat-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)}))
owner-id (:owner-id command)
bot-db (get bot-db chat-id)
params (merge (input-model/args->params content)
{:bot-db bot-db
:metadata metadata})
command-message {:command command
:params params
:to-message (:to-message-id metadata)
:created-at current-time
:id message-id
:chat-id chat-id
:jail-id (or owner-id jail-id)}
request-data {:message-id message-id
:chat-id chat-id
:jail-id (or owner-id jail-id)
:content {:command (:name command)
:params params
:type (:type command)}
:on-requested (fn [jail-response]
(event-after-creator command-message jail-response))}]
{:dispatch [:request-command-data request-data data-type]})))
(register-handler-fx
:send-current-message
(fn [{{:keys [current-chat-id] :as db} :db} _]
(let [chat-command (input-model/selected-chat-command db current-chat-id)
seq-command? (get-in chat-command [:command :sequential-params])
chat-command (if seq-command?
(let [args (get-in db [:chats current-chat-id :seq-arguments])]
(assoc chat-command :args args))
(update chat-command :args #(remove str/blank? %)))
set-chat-ui-props-event [:set-chat-ui-props {:sending-in-progress? true}]
additional-events (if (:command chat-command)
(if (= :complete (input-model/command-completion chat-command))
[[:proceed-command chat-command current-chat-id]
[:clear-seq-arguments current-chat-id]]
(let [text (get-in db [:chats current-chat-id :input-text])]
[[:set-chat-ui-props {:sending-in-progress? false}]
(when-not (input-model/text-ends-with-space? text)
[:set-chat-input-text (str text const/spacing-char)])]))
[[::send-message nil current-chat-id]])]
{:dispatch-n (into [set-chat-ui-props-event]
(remove nil? additional-events))})))
(register-handler-fx
::check-dapp-suggestions
[trim-v]
(fn [{{:keys [current-account-id] :as db} :db} [chat-id text]]
(let [data (get-in db [:local-storage chat-id])]
{:chat-fx/call-jail-function {:chat-id chat-id
:function :on-message-input-change
:parameters {:message text}
:context {:data data
:from current-account-id}}})))
(register-handler-db
:clear-seq-arguments
[trim-v]
(fn [{:keys [current-chat-id chats] :as db} [chat-id]]
(let [chat-id (or chat-id current-chat-id)]
(-> db
(assoc-in [:chats chat-id :seq-arguments] [])
(assoc-in [:chats chat-id :seq-argument-input-text] nil)))))
(register-handler-db
::update-seq-arguments
[trim-v]
(fn [{:keys [current-chat-id chats] :as db} [chat-id]]
(let [chat-id (or chat-id current-chat-id)
text (get-in chats [chat-id :seq-argument-input-text])]
(-> db
(update-in [:chats chat-id :seq-arguments] #(into [] (conj % text)))
(assoc-in [:chats chat-id :seq-argument-input-text] nil)))))
(register-handler-fx
:send-seq-argument
[trim-v]
(fn [{{:keys [current-chat-id chats] :as db} :db} [chat-id]]
(let [chat-id (or chat-id current-chat-id)
text (get-in chats [chat-id :seq-argument-input-text])
seq-arguments (get-in chats [chat-id :seq-arguments])
command (-> (input-model/selected-chat-command db chat-id)
(assoc :args (into [] (conj seq-arguments text))))]
{:dispatch [::request-command-data
{:content command
:chat-id chat-id
:jail-id (or (get-in command [:command :bot]) chat-id)
:data-type :validator
:event-after-creator (fn [_ jail-response]
[::proceed-validation
jail-response
[[::update-seq-arguments chat-id]
[:send-current-message]]])}]})))
(register-handler-db
:set-chat-seq-arg-input-text
[trim-v]
(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))))
(register-handler-fx
:update-text-selection
[trim-v]
(fn [{{:keys [current-chat-id] :as db} :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)]
(cond-> {:dispatch-n [[:set-chat-ui-props {:selection selection}]
[:load-chat-parameter-box (:command command)]]}
(and (= selection (+ (count const/command-char)
(count (get-in command [:command :name]))
(count const/spacing-char)))
(get-in command [:command :sequential-params]))
(update :dispatch-n conj [:chat-input-focus :seq-input-ref])))))
(register-handler-fx
:select-prev-argument
(fn [{{:keys [chat-ui-props current-chat-id] :as db} :db} _]
(let [input-text (get-in db [:chats current-chat-id :input-text])
command (input-model/selected-chat-command db current-chat-id input-text)]
(if (get-in command [:command :sequential-params])
{:dispatch-n [[:set-command-argument [0 "" false]]
[:set-chat-seq-arg-input-text ""]
[:load-chat-parameter-box (:command command)]]}
(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])]
{::set-native-props {:ref ref
:props {:selection {:start new-sel :end new-sel}}}
:dispatch [:update-text-selection new-sel]})))))))
(register-handler-fx
:select-next-argument
(fn [{{:keys [chat-ui-props current-chat-id] :as db} :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
(min (count input-text)))
ref (get-in chat-ui-props [current-chat-id :input-ref])]
{::set-native-props {:ref ref
:props {:selection {:start new-sel :end new-sel}}}
:dispatch [:update-text-selection new-sel]}))))

View File

@ -28,8 +28,8 @@
[status-im.utils.types :refer [json->clj]] [status-im.utils.types :refer [json->clj]]
[status-im.chat.utils :refer [console? not-console? safe-trim]] [status-im.chat.utils :refer [console? not-console? safe-trim]]
[status-im.utils.gfycat.core :refer [generate-gfy]] [status-im.utils.gfycat.core :refer [generate-gfy]]
status-im.chat.handlers.input status-im.chat.events.input
status-im.chat.handlers.commands status-im.chat.events.commands
status-im.chat.handlers.animation status-im.chat.handlers.animation
status-im.chat.handlers.requests status-im.chat.handlers.requests
status-im.chat.handlers.unviewed-messages status-im.chat.handlers.unviewed-messages
@ -186,14 +186,7 @@
(defn load-messages! (defn load-messages!
([db] (load-messages! db nil)) ([db] (load-messages! db nil))
([{:keys [current-chat-id] :as db} _] ([{:keys [current-chat-id] :as db} _]
(let [messages (messages/get-by-chat-id current-chat-id)] (let [messages (messages/get-by-chat-id current-chat-id)]
(doseq [{:keys [content] :as message} messages]
(when (and (:command content)
(not (:content content)))
;; todo rewrite it so that commands defined outside chat's context
;; (bots' commands in group chats and global commands in all chats)
;; could be rendered properly
(dispatch [:request-command-data (assoc message :chat-id current-chat-id)])))
(assoc db :messages messages)))) (assoc db :messages messages))))
(defn init-chat (defn init-chat

View File

@ -1,84 +0,0 @@
(ns status-im.chat.handlers.commands
(:require [cljs.reader :as reader]
[clojure.string :as str]
[re-frame.core :refer [enrich after dispatch]]
[status-im.data-store.messages :as messages]
[status-im.utils.handlers :as handlers]
[status-im.components.status :as status]
[status-im.chat.constants :as const]
[status-im.commands.utils :as cu]
[status-im.i18n :as i18n]
[status-im.utils.platform :as platform]
[taoensso.timbre :as log]))
(defn generate-context [{:keys [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 [current-account-id chats]
:contacts/keys [contacts] :as db}
[_ {{:keys [command params content-command type]} :content
:keys [message-id chat-id jail-id on-requested from] :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'
#(dispatch [:request-command-data message data-type])])
(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 (generate-context db chat-id to)}
callback #(let [result (get-in % [:result :returned])
result' (if (:markup result)
(update result :markup cu/generate-hiccup)
result)]
;; don't fill message data with nil results
(when result'
(dispatch [:set-in [:message-data data-type message-id] result']))
(when (and result (= :preview data-type))
;; update message in realm with serialized preview
(messages/update {:message-id message-id
:preview (prn-str result)}))
(when on-requested (on-requested result')))]
;chat-id path params callback lock? type
(status/call-jail {:jail-id jail-id'
:path path
:params params
:callback callback})))))))
(handlers/register-handler :execute-command-immediately
(handlers/side-effect!
(fn [_ [_ {command-name :name :as command}]]
(case (keyword command-name)
:grant-permissions
(dispatch [:request-permissions
[:read-external-storage]
#(dispatch [:initialize-geth])])
(log/debug "ignoring command: " command)))))
(handlers/register-handler :request-command-preview
(handlers/side-effect!
(fn [db [_ {:keys [message-id] :as message}]]
(let [previews (get-in db [:message-data :preview])]
(when-not (contains? previews message-id)
(let [{serialized-preview :preview} (messages/get-by-id message-id)]
;; if preview is already cached in db, do not request it from jail
;; and write it directly to message-data path
(if serialized-preview
(dispatch [:set-in [:message-data :preview message-id]
(-> serialized-preview
reader/read-string
(update :markup cu/generate-hiccup))])
(dispatch [:request-command-data message :preview]))))))))

View File

@ -1,396 +0,0 @@
(ns status-im.chat.handlers.input
(:require [re-frame.core :refer [enrich after dispatch]]
[taoensso.timbre :as log]
[status-im.chat.constants :as const]
[status-im.chat.utils :as chat-utils]
[status-im.chat.models.input :as input-model]
[status-im.chat.models.suggestions :as suggestions]
[status-im.components.react :as react-comp]
[status-im.components.status :as status]
[status-im.utils.datetime :as time]
[status-im.utils.handlers :as handlers]
[status-im.utils.random :as random]
[status-im.i18n :as i18n]
[clojure.string :as str]))
(handlers/register-handler
:update-input-data
(fn [db]
(input-model/modified-db-after-change db)))
(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)]
(dispatch [:update-suggestions chat-id text])
(->> text
(input-model/text->emoji)
(assoc-in db [:chats chat-id :input-text])))))
(handlers/register-handler :add-to-chat-input-text
(handlers/side-effect!
(fn [{:keys [chats current-chat-id]} [_ text-to-add]]
(let [input-text (get-in chats [current-chat-id :input-text])]
(dispatch [:set-chat-input-text (str input-text text-to-add)])))))
(handlers/register-handler :select-chat-input-command
(handlers/side-effect!
(fn [{:keys [current-chat-id chat-ui-props] :as db}
[_ {: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
:prev-command name}])
(dispatch [:load-chat-parameter-box command 0])
(if sequential-params
(js/setTimeout
#(do (when-not prevent-auto-focus?
(dispatch [:chat-input-focus :seq-input-ref]))
(dispatch [:set-chat-seq-arg-input-text (str/join const/spacing-char prefill)]))
100)
(when-not prevent-auto-focus?
(dispatch [:chat-input-focus :input-ref]))))))
(handlers/register-handler :set-chat-input-metadata
(fn [{:keys [current-chat-id] :as db} [_ data chat-id]]
(let [chat-id (or chat-id current-chat-id)]
(assoc-in db [:chats chat-id :input-metadata] data))))
(handlers/register-handler :set-command-argument
(handlers/side-effect!
(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 [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)
(conj command-args arg))]
(dispatch [:set-chat-input-text (str command-name
const/spacing-char
(input-model/join-command-args command-args)
(when (and move-to-next?
(= index (dec (count command-args))))
const/spacing-char))])))))))
(handlers/register-handler :chat-input-focus
(handlers/side-effect!
(fn [{:keys [current-chat-id chat-ui-props] :as db} [_ ref]]
(try
(when-let [ref (get-in chat-ui-props [current-chat-id ref])]
(.focus ref))
(catch :default e
(log/debug "Cannot focus the reference"))))))
(handlers/register-handler :chat-input-blur
(handlers/side-effect!
(fn [{:keys [current-chat-id chat-ui-props] :as db} [_ ref]]
(try
(when-let [ref (get-in chat-ui-props [current-chat-id ref])]
(.blur ref))
(catch :default e
(log/debug "Cannot blur the reference"))))))
(handlers/register-handler :update-suggestions
(fn [{:keys [current-chat-id] :as db} [_ chat-id text]]
(let [chat-id (or chat-id current-chat-id)
chat-text (str/trim (or text (get-in db [:chats chat-id :input-text]) ""))
requests (->> (suggestions/get-request-suggestions db chat-text)
(remove (fn [{:keys [type]}]
(= type :grant-permissions))))
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/contacts chat-id])]
(when dapp?
(if (str/blank? chat-text)
(dispatch [:set-in [:chats chat-id :parameter-boxes :message] nil])
(when (every? empty? [requests commands])
(dispatch [::check-dapp-suggestions chat-id chat-text]))))
(-> db
(assoc-in [:chats chat-id :request-suggestions] requests)
(assoc-in [:chats chat-id :command-suggestions] all-commands)))))
(handlers/register-handler :load-chat-parameter-box
(handlers/side-effect!
(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 [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
parameter-index
:suggestions]
args (-> (get-in db [:chats current-chat-id :input-text])
(input-model/split-command-args)
(rest))
seq-arg (get-in db [:chats current-chat-id :seq-argument-input-text])
to (get-in db [:contacts/contacts current-chat-id :address])
params {:parameters {:args args
:bot-db bot-db
:seq-arg seq-arg}
:context (merge {:data data
:from current-account-id
:to to}
(input-model/command-dependent-context-params current-chat-id command))}]
(status/call-jail
{:jail-id (or bot owner-id current-chat-id)
:path path
:params params
:callback #(dispatch [:received-bot-response
{:chat-id current-chat-id
:command command
:parameter-index parameter-index}
%])})))))))
(handlers/register-handler ::send-message
(handlers/side-effect!
(fn [{:keys [current-public-key current-account-id] :as db} [_ command chat-id]]
(let [text (get-in db [:chats chat-id :input-text])
data {:message text
:command command
:chat-id chat-id
:identity current-public-key
:address current-account-id}]
(dispatch [:set-chat-input-text nil chat-id])
(dispatch [:set-chat-input-metadata nil chat-id])
(dispatch [:set-chat-ui-props {:sending-in-progress? false}])
(cond
command
(dispatch [:check-commands-handlers! data])
(not (str/blank? text))
(dispatch [:prepare-message data]))))))
(handlers/register-handler :proceed-command
(handlers/side-effect!
(fn [db [_ {{:keys [bot]} :command :as content} chat-id]]
(let [params {:content content
:chat-id chat-id
:jail-id (or bot chat-id)}
on-send-params (merge params
{:data-type :on-send
:after #(dispatch [::send-command %2 content chat-id])})
after-validation #(dispatch [::request-command-data on-send-params])
validation-params (merge params
{:data-type :validator
:after #(dispatch [::proceed-validation %2 after-validation])})]
(dispatch [::request-command-data validation-params])))))
(handlers/register-handler ::proceed-validation
(handlers/side-effect!
(fn [db [_ {:keys [markup validationHandler parameters]} proceed-fn]]
(let [set-errors #(do (dispatch [:set-chat-ui-props {:validation-messages %
:sending-in-progress? false}]))]
(cond
markup
(set-errors markup)
validationHandler
(do (dispatch [::execute-validation-handler validationHandler parameters set-errors proceed-fn])
(dispatch [:set-chat-ui-props {:sending-in-progress? false}]))
:default
(proceed-fn))))))
(handlers/register-handler ::execute-validation-handler
(handlers/side-effect!
(fn [_ [_ name params set-errors proceed]]
(when-let [validator (input-model/validation-handler name)]
(validator params set-errors proceed)))))
(handlers/register-handler ::send-command
(handlers/side-effect!
(fn [db [_ on-send {{:keys [fullscreen bot]} :command :as content} chat-id]]
(if on-send
(do
(when fullscreen
(dispatch [:choose-predefined-expandable-height :result-box :max]))
(dispatch [:set-chat-ui-props {:result-box on-send
:sending-in-progress? false}])
(react-comp/dismiss-keyboard!))
(dispatch [::request-command-data
{:content content
:chat-id chat-id
:jail-id (or bot chat-id)
:data-type :preview
:after #(dispatch [::send-message % chat-id])}])))))
(handlers/register-handler ::request-command-data
(handlers/side-effect!
(fn [{:keys [bot-db]
:contacts/keys [contacts] :as db}
[_ {{:keys [command
metadata
args]
:as content} :content
:keys [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)}))
owner-id (:owner-id command)
bot-db (get bot-db chat-id)
params (merge (input-model/args->params content)
{:bot-db bot-db
:metadata metadata})
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 (or owner-id jail-id)}
request-data {:message-id message-id
:chat-id chat-id
:jail-id (or owner-id jail-id)
:content {:command (:name command)
:params params
:type (:type command)}
:on-requested #(after command-message %)}]
(dispatch [:request-command-data request-data data-type])))))
(handlers/register-handler :send-current-message
(handlers/side-effect!
(fn [{:keys [current-chat-id] :as db} [_ chat-id]]
(dispatch [:set-chat-ui-props {:sending-in-progress? true}])
(let [chat-id (or chat-id current-chat-id)
chat-command (input-model/selected-chat-command db chat-id)
seq-command? (get-in chat-command [:command :sequential-params])
chat-command (if seq-command?
(let [args (get-in db [:chats chat-id :seq-arguments])]
(assoc chat-command :args args))
(update chat-command :args #(remove str/blank? %)))]
(if (:command chat-command)
(if (= :complete (input-model/command-completion chat-command))
(do
(dispatch [:proceed-command chat-command chat-id])
(dispatch [:clear-seq-arguments chat-id]))
(let [text (get-in db [:chats chat-id :input-text])]
(dispatch [:set-chat-ui-props {:sending-in-progress? false}])
(when-not (input-model/text-ends-with-space? text)
(dispatch [:set-chat-input-text (str text const/spacing-char)]))))
(dispatch [::send-message nil chat-id]))))))
(handlers/register-handler ::check-dapp-suggestions
(handlers/side-effect!
(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
:from current-account-id}})))))
(handlers/register-handler :clear-seq-arguments
(fn [{:keys [current-chat-id chats] :as db} [_ chat-id]]
(let [chat-id (or chat-id current-chat-id)]
(-> db
(assoc-in [:chats chat-id :seq-arguments] [])
(assoc-in [:chats chat-id :seq-argument-input-text] nil)))))
(handlers/register-handler :update-seq-arguments
(fn [{:keys [current-chat-id chats] :as db} [_ chat-id]]
(let [chat-id (or chat-id current-chat-id)
text (get-in chats [chat-id :seq-argument-input-text])]
(-> db
(update-in [:chats chat-id :seq-arguments] #(into [] (conj % text)))
(assoc-in [:chats chat-id :seq-argument-input-text] nil)))))
(handlers/register-handler :send-seq-argument
(handlers/side-effect!
(fn [{:keys [current-chat-id chats] :as db} [_ chat-id]]
(let [chat-id (or chat-id current-chat-id)
text (get-in chats [chat-id :seq-argument-input-text])
seq-arguments (get-in db [:chats chat-id :seq-arguments])
command (-> (input-model/selected-chat-command db chat-id)
(assoc :args (into [] (conj seq-arguments text))))
args (get-in chats [chat-id :seq-arguments])
after-validation #(do
(dispatch [:update-seq-arguments chat-id])
(dispatch [:send-current-message]))]
(dispatch [::request-command-data
{:content command
:chat-id chat-id
:jail-id (or (get-in command [:command :bot]) chat-id)
:data-type :validator
:after #(dispatch [::proceed-validation %2 after-validation])}])))))
(handlers/register-handler :set-chat-seq-arg-input-text
(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 [input-text (get-in db [:chats current-chat-id :input-text])
command (input-model/selected-chat-command db current-chat-id input-text)]
(if (get-in command [:command :sequential-params])
(do
(dispatch [:set-command-argument [0 "" false]])
(dispatch [:set-chat-seq-arg-input-text ""])
(dispatch [:load-chat-parameter-box (:command command)]))
(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
(min (count input-text)))
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

@ -267,9 +267,8 @@
(defmethod validation-handler :phone (defmethod validation-handler :phone
[_] [_]
(fn [[number] set-errors proceed] (fn [[number] error-events-creator]
(if (phone-number/valid-mobile-number? number) (when-not (phone-number/valid-mobile-number? number)
(proceed) (error-events-creator [validation-message
(set-errors [validation-message {:title (i18n/label :t/phone-number)
{:title (i18n/label :t/phone-number) :description (i18n/label :t/invalid-phone)}]))))
:description (i18n/label :t/invalid-phone)}]))))

View File

@ -1,23 +1,30 @@
(ns status-im.chat.subs (ns status-im.chat.subs
(:require [re-frame.core :refer [reg-sub dispatch subscribe path]] (:require [re-frame.core :refer [reg-sub dispatch subscribe path]]
[status-im.data-store.chats :as chats] [status-im.data-store.chats :as chats]
[status-im.chat.constants :as const] [status-im.chat.constants :as const]
[status-im.chat.models.input :as input-model] [status-im.chat.models.input :as input-model]
[status-im.chat.utils :as chat-utils] [status-im.chat.utils :as chat-utils]
[status-im.chat.views.input.utils :as input-utils] [status-im.chat.views.input.utils :as input-utils]
[status-im.constants :refer [response-suggesstion-resize-duration [status-im.constants :refer [response-suggesstion-resize-duration
content-type-status content-type-status
console-chat-id]] console-chat-id]]
[status-im.commands.utils :as commands-utils]
[status-im.models.commands :as commands] [status-im.models.commands :as commands]
[status-im.utils.platform :refer [platform-specific ios?]] [status-im.utils.platform :refer [platform-specific ios?]]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[clojure.string :as str])) [clojure.string :as str]))
(reg-sub (reg-sub
:chat-ui-props :chat-ui-props
(fn [db [_ ui-element chat-id]] (fn [db [_ ui-element chat-id]]
(let [current-chat-id (subscribe [:get-current-chat-id])] (let [current-chat-id (subscribe [:get-current-chat-id])
(get-in db [:chat-ui-props (or chat-id @current-chat-id) ui-element])))) data (get-in db [:chat-ui-props (or chat-id @current-chat-id) ui-element])]
(cond-> data
(:markup data)
(update :markup commands-utils/generate-hiccup)
(and (= ui-element :validation-messages) data)
commands-utils/generate-hiccup))))
(reg-sub (reg-sub
:chat-input-margin :chat-input-margin
@ -181,10 +188,16 @@
(filter :show?) (filter :show?)
(first))))) (first)))))
(reg-sub :get-last-message-short-preview (reg-sub :get-message-short-preview-markup
(fn [db [_ message-id]]
(get-in db [:message-data :short-preview message-id :markup])))
(reg-sub :get-last-message-short-preview
(fn [db [_ chat-id]] (fn [db [_ chat-id]]
(let [last-message (subscribe [:get-last-message chat-id])] (let [last-message (subscribe [:get-last-message chat-id])
(get-in db [:message-data :short-preview (:message-id @last-message)])))) preview (subscribe [:get-message-short-preview-markup (:message-id @last-message)])]
(when-let [markup @preview]
(commands-utils/generate-hiccup markup)))))
(reg-sub :get-default-container-area-height (reg-sub :get-default-container-area-height
:<- [:chat-ui-props :input-height] :<- [:chat-ui-props :input-height]
@ -213,3 +226,13 @@
(filter :outgoing) (filter :outgoing)
(sort-by :clock-value >) (sort-by :clock-value >)
(first)))) (first))))
(reg-sub :get-message-preview-markup
(fn [db [_ message-id]]
(get-in db [:message-data :preview message-id :markup])))
(reg-sub :get-message-preview
(fn [db [_ message-id]]
(let [preview (subscribe [:get-message-preview-markup message-id])]
(when-let [markup @preview]
(commands-utils/generate-hiccup markup)))))

View File

@ -136,7 +136,7 @@
global-commands [:get :global-commands] global-commands [:get :global-commands]
current-chat-id [:get-current-chat-id] current-chat-id [:get-current-chat-id]
contact-chat [:get-in [:chats (if outgoing to from)]] contact-chat [:get-in [:chats (if outgoing to from)]]
preview [:get-in [:message-data :preview message-id :markup]]] preview [:get-message-preview message-id]]
(let [commands (merge commands from-commands) (let [commands (merge commands from-commands)
{:keys [command params]} (parse-command-message-content commands global-commands content) {:keys [command params]} (parse-command-message-content commands global-commands content)
{:keys [name type] {:keys [name type]
@ -386,7 +386,7 @@
(defn chat-message [{:keys [outgoing message-id chat-id user-statuses from] :as message}] (defn chat-message [{:keys [outgoing message-id chat-id user-statuses from] :as message}]
(let [my-identity (subscribe [:get :current-public-key]) (let [my-identity (subscribe [:get :current-public-key])
status (subscribe [:get-in [:message-data :user-statuses message-id my-identity]]) status (subscribe [:get-in [:message-data :user-statuses message-id my-identity]])
preview (subscribe [:get-in [:message-data :preview message-id :markup]])] preview (subscribe [:get-message-preview message-id])]
(r/create-class (r/create-class
{:display-name "chat-message" {:display-name "chat-message"
:component-will-mount :component-will-mount

View File

@ -71,7 +71,7 @@
(let [commands-atom (subscribe [:get-commands-and-responses chat-id]) (let [commands-atom (subscribe [:get-commands-and-responses chat-id])
answered? (subscribe [:is-request-answered? message-id]) answered? (subscribe [:is-request-answered? message-id])
status-initialized? (subscribe [:get :status-module-initialized?]) status-initialized? (subscribe [:get :status-module-initialized?])
markup (subscribe [:get-in [:message-data :preview message-id :markup]])] markup (subscribe [:get-message-preview message-id])]
(fn [{:keys [message-id content from incoming-group]}] (fn [{:keys [message-id content from incoming-group]}]
(let [commands @commands-atom (let [commands @commands-atom
{:keys [prefill prefill-bot-db prefillBotDb params] {:keys [prefill prefill-bot-db prefillBotDb params]

View File

@ -9,8 +9,7 @@
[status-im.models.commands :as cm] [status-im.models.commands :as cm]
[status-im.constants :refer [console-chat-id]] [status-im.constants :refer [console-chat-id]]
[status-im.i18n :refer [get-contact-translated]] [status-im.i18n :refer [get-contact-translated]]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[status-im.commands.utils :as cu]
[status-im.data-store.local-storage :as local-storage])) [status-im.data-store.local-storage :as local-storage]))
(defn command-handler! (defn command-handler!
@ -22,7 +21,7 @@
(cond (cond
handler-error handler-error
(when-let [markup (:markup handler-error)] (when-let [markup (:markup handler-error)]
(dispatch [:set-chat-ui-props {:validation-messages (cu/generate-hiccup markup)}])) (dispatch [:set-chat-ui-props {:validation-messages markup}]))
result result
(let [command' (assoc command :handler-data returned) (let [command' (assoc command :handler-data returned)

View File

@ -50,7 +50,7 @@
(:content content)] (:content content)]
(:command content) (:command content)
(:markup preview) preview
:else :else
[text {:style st/last-message-text [text {:style st/last-message-text

View File

@ -443,4 +443,4 @@
[:add-pending-contact id] [:add-pending-contact id]
[:add-new-contact-and-open-chat {:name (generate-gfy) [:add-new-contact-and-open-chat {:name (generate-gfy)
:photo-path (identicon id) :photo-path (identicon id)
:whisper-identity id}])}))) :whisper-identity id}])})))