Comments for input model; redundant functions has been removed; refactoring

This commit is contained in:
alwx 2017-07-15 13:08:25 +02:00 committed by Roman Volosovskyi
parent 2adc02849f
commit 6669f79c44
10 changed files with 152 additions and 113 deletions

View File

@ -13,25 +13,19 @@
[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]))
;; 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-input-text (input-model/make-input-text text-splitted old-args)]
(assoc-in db [:chats chat-id :input-text] new-input-text))
(->> text
(input-model/text->emoji)
(assoc-in db [:chats chat-id :input-text]))))))
(assoc-in db [:chats chat-id :input-text])))))
(handlers/register-handler :add-to-chat-input-text
(handlers/side-effect!
@ -157,7 +151,7 @@
:context (merge {:data data
:from current-account-id
:to to}
(input-model/command-dependent-context-params command))}]
(input-model/command-dependent-context-params current-chat-id command))}]
(status/call-jail
{:jail-id (or bot owner-id current-chat-id)
:path path
@ -341,7 +335,7 @@
(dispatch [::request-command-data
{:content command
:chat-id chat-id
;;TODO(alwx): jail-id ?
:jail-id (or (get-in command [:command :bot]) chat-id)
:data-type :validator
:after #(dispatch [::proceed-validation %2 after-validation])}])))))

View File

@ -1,32 +1,52 @@
(ns status-im.chat.models.input
(:require [clojure.string :as str]
[status-im.components.react :as rc]
[status-im.components.status :as status]
[status-im.chat.constants :as const]
[status-im.chat.views.input.validation-messages :refer [validation-message]]
[status-im.i18n :as i18n]
[status-im.utils.phone-number :as phone-number]
[taoensso.timbre :as log]
[status-im.chat.utils :as chat-utils]
[status-im.bots.constants :as bots-constants]))
[status-im.bots.constants :as bots-constants]
[taoensso.timbre :as log]))
(def emojis (js/require "emojilib"))
(defn text->emoji [text]
(defn text->emoji
"Replaces emojis in a specified `text`"
[text]
(when text
(str/replace text
#":([a-z_\-+0-9]*):"
(fn [[original emoji-id]]
(if-let [emoji-map (aget emojis "lib" emoji-id)]
(if-let [emoji-map (aget rc/emojilib "lib" emoji-id)]
(aget emoji-map "char")
original)))))
(defn text-ends-with-space? [text]
(defn text-ends-with-space?
"Returns true if the last symbol of `text` is space"
[text]
(when text
(= (str/last-index-of text const/spacing-char)
(dec (count text)))))
(defn possible-chat-actions [{:keys [global-commands] :as db} chat-id]
(let [{:keys [contacts requests]} (get-in db [:chats chat-id])]
(defn starts-as-command?
"Returns true if `text` may be treated as a command.
To make sure that text is command we need to use `possible-chat-actions` function."
[text]
(and (not (nil? text))
(or (str/starts-with? text const/bot-char)
(str/starts-with? text const/command-char))))
(defn possible-chat-actions
"Returns a map of possible chat actions (commands and response) for a specified `chat-id`.
Every map's key is a command's name, value is a pair of [`command` `message-id`]. In the case
of commands `message-id` is `:any`, for responses value contains the actual id.
Example of output:
{:browse [{:description \"Launch the browser\" :name \"browse\" ...} :any]
:request [{:description \"Request a payment\" :name \"request\" ...} \"message-id\"]}"
[{:keys [global-commands current-chat-id] :as db} chat-id]
(let [chat-id (or chat-id current-chat-id)
{:keys [contacts requests]} (get-in db [:chats chat-id])]
(->> contacts
(map (fn [{:keys [identity]}]
(let [{:keys [commands responses]} (get-in db [:contacts identity])]
@ -36,10 +56,20 @@
requests)]
(into commands' responses')))))
(reduce (fn [m cur] (into (or m {}) cur)))
(into {})
(vals))))
(into {}))))
(defn split-command-args [command-text]
(defn split-command-args
"Returns a list of command's arguments including the command's name.
Examples:
Input: '/send Jarrad 1.0'
Output: ['/send' 'Jarrad' '1.0']
Input: '/send \"Complex name with space in between\" 1.0'
Output: ['/send' 'Complex name with space in between' '1.0']
All the complex logic inside this function aims to support wrapped arguments."
[command-text]
(let [space? (text-ends-with-space? command-text)
command-text (if space?
(str command-text ".")
@ -66,6 +96,14 @@
(first))))
(defn join-command-args [args]
"Transforms a list of args to a string. The opposite of `split-command-args`.
Examples:
Input: ['/send' 'Jarrad' '1.0']
Output: '/send Jarrad 1.0'
Input: ['/send' 'Complex name with space in between' '1.0']
Output: '/send \"Complex name with space in between\" 1.0'"
(->> args
(map (fn [arg]
(if (not (str/index-of arg const/spacing-char))
@ -74,6 +112,14 @@
(str/join const/spacing-char)))
(defn selected-chat-command
"Returns a map containing `:command`, `:metadata` and `:args` keys.
Can also return `nil` if there is no selected command.
* `:command` key contains a map with all information about command.
* `:metadata` is also a map which contains some additional information, usually not visible by user.
For instance, we can add a `:to-message-id` key to this map, and this key will allow us to identity
the request we're responding to.
* `:args` contains all arguments provided by user."
([{:keys [current-chat-id] :as db} chat-id input-text]
(let [chat-id (or chat-id current-chat-id)
input-metadata (get-in db [:chats chat-id :input-metadata])
@ -81,12 +127,12 @@
possible-actions (possible-chat-actions db chat-id)
command-args (split-command-args input-text)
command-name (first command-args)]
(when (chat-utils/starts-as-command? (or command-name ""))
(when (starts-as-command? (or command-name ""))
(when-let [[command to-message-id]
(-> (filter (fn [[{:keys [name bot]} message-id]]
(= (or (when-not (bots-constants/mailman-bot? bot) bot) name)
(subs command-name 1)))
possible-actions)
(vals possible-actions))
(first))]
{:command command
:metadata (if (and (nil? (:to-message-id input-metadata)) (not= :any to-message-id))
@ -101,6 +147,8 @@
(def *no-argument-error* -1)
(defn current-chat-argument-position
"Returns the position of current argument. It's just an integer number from -1 to infinity.
-1 (`*no-argument-error*`) means error. It can happen if there is no selected command or selection."
[{:keys [args] :as command} input-text selection seq-arguments]
(if command
(if (get-in command [:command :sequential-params])
@ -120,7 +168,12 @@
*no-argument-error*)))
*no-argument-error*))
(defn argument-position [{:keys [current-chat-id] :as db} chat-id]
(defn argument-position
"Returns the position of current argument. It's just an integer from -1 to infinity.
-1 (`*no-argument-error*`) means error. It can happen if there is no command or selection.
This method is basically just another way of calling `current-chat-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])
@ -129,6 +182,11 @@
(current-chat-argument-position chat-command input-text selection seq-arguments)))
(defn command-completion
"Returns one of the following values indicating a command's completion status:
* `:complete` means that the command is complete and can be sent;
* `:less-than-needed` means that the command is not complete and additional arguments should be provided;
* `:more-than-needed` means that the command is more than complete and contains redundant arguments;
* `:no-command` means that there is no selected command."
([{: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])
@ -154,7 +212,11 @@
:no-command)
:no-command))))
(defn args->params [{:keys [command args]}]
(defn args->params
"Uses `args` (which is a list or vector like ['Jarrad' '1.0']) and command's `params`
and returns a map that looks the following way:
{:recipient \"Jarrad\" :amount \"1.0\"}"
[{:keys [command args]}]
(let [params (:params command)]
(->> args
(map-indexed (fn [i value]
@ -162,11 +224,39 @@
(into {}))))
(defn command-dependent-context-params
[{:keys [name] :as command}]
(case name
"phone" {:suggestions (phone-number/get-examples)}
"Returns additional `:context` data that will be added to specific commands.
The following data shouldn't be hardcoded here."
[chat-id {:keys [name] :as command}]
(case chat-id
"console" (case name
"phone" {:suggestions (phone-number/get-examples)}
{})
{}))
(defn modified-db-after-change
"Returns the new db object that should be used after any input change."
[{:keys [current-chat-id] :as db}]
(let [input-text (get-in db [:chats current-chat-id :input-text])
command (selected-chat-command db current-chat-id input-text)
prev-command (get-in db [:chat-ui-props current-chat-id :prev-command])]
(if command
(cond-> db
;; clear the bot db
(not= prev-command (-> command :command :name))
(assoc-in [:bot-db (or (:bot command) current-chat-id)] nil)
;; clear the chat's validation messages
true
(assoc-in [:chat-ui-props current-chat-id :validation-messages] nil))
(-> db
;; clear input metadata
(assoc-in [:chats current-chat-id :input-metadata] nil)
;; clear
(update-in [:chat-ui-props current-chat-id]
merge
{:result-box nil
:validation-messages nil
:prev-command (-> command :command :name)})))))
(defmulti validation-handler (fn [name] (keyword name)))
(defmethod validation-handler :phone
@ -176,31 +266,4 @@
(proceed)
(set-errors [validation-message
{:title (i18n/label :t/phone-number)
:description (i18n/label :t/invalid-phone)}]))))
(defn- changed-arg-position [xs ys]
(let [longest (into [] (max-key count xs ys))
shortest (into [] (if (= longest xs) ys xs))]
(->> longest
(map-indexed (fn [index x]
(if (and (> (count shortest) index)
(= (count x) (count (get shortest index))))
nil
index)))
(remove nil?)
(first))))
(defn make-input-text [[command & args] old-args]
(let [args (into [] args)
old-args (into [] old-args)
arg-pos (changed-arg-position args old-args)
new-arg (get args arg-pos)
new-args (if arg-pos
(assoc old-args arg-pos (when new-arg
(str/trim new-arg)))
old-args)]
(str
command
const/spacing-char
(join-command-args new-args))))
:description (i18n/label :t/invalid-phone)}]))))

View File

@ -73,13 +73,6 @@
(merge responses commands))))
(apply merge)))))
(reg-sub :possible-chat-actions
(fn [db [_ chat-id]]
"Returns a vector of [command message-id] values. `message-id` can be `:any`.
Example: [[browse-command :any] [debug-command :any] [phone-command '1489161286111-58a2cd...']]"
(let [chat-id (or chat-id (db :current-chat-id))]
(input-model/possible-chat-actions db chat-id))))
(reg-sub
:selected-chat-command
(fn [db [_ chat-id]]
@ -138,7 +131,7 @@
selected-command (subscribe [:selected-chat-command chat-id])
requests (subscribe [:chat :request-suggestions chat-id])
commands (subscribe [:chat :command-suggestions chat-id])]
(and (or @show-suggestions? (chat-utils/starts-as-command? (str/trim (or @input-text ""))))
(and (or @show-suggestions? (input-model/starts-as-command? (str/trim (or @input-text ""))))
(not (:command @selected-command))
(or (not-empty @requests)
(not-empty @commands))))))

View File

@ -56,8 +56,3 @@
bot (str const/bot-char bot)
:else (str const/command-char name)))
(defn starts-as-command? [text]
(and (not (nil? text))
(or (str/starts-with? text const/bot-char)
(str/starts-with? text const/command-char))))

View File

@ -1,4 +1,4 @@
(ns status-im.chat.views.choosers.choose-contact
(ns status-im.chat.views.api.choose-contact
(:require-macros [status-im.utils.views :refer [defview]])
(:require [reagent.core :as r]
[re-frame.core :refer [dispatch subscribe]]

View File

@ -1,4 +1,4 @@
(ns status-im.chat.views.geolocation.styles
(ns status-im.chat.views.api.geolocation.styles
(:require [status-im.components.styles :as common]))
(defn place-item-container [address]

View File

@ -1,4 +1,4 @@
(ns status-im.chat.views.geolocation.views
(ns status-im.chat.views.api.geolocation.views
(:require-macros [status-im.utils.views :refer [defview letsubs]]
[reagent.ratom :refer [reaction]])
(:require [status-im.components.react :refer [view image text touchable-highlight]]
@ -6,7 +6,7 @@
[goog.string :as gstr]
[status-im.utils.utils :refer [http-get]]
[status-im.utils.types :refer [json->clj]]
[status-im.chat.views.geolocation.styles :as st]
[status-im.chat.views.api.geolocation.styles :as st]
[status-im.components.mapbox :refer [mapview]]
[re-frame.core :refer [dispatch subscribe]]
[status-im.i18n :refer [label]]

View File

@ -64,9 +64,8 @@
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?]}]
(fn [{:keys [set-layout-height-fn set-container-width-fn height single-line-input?]}]
[text-input
{:ref #(when %
(dispatch [:set-chat-ui-props {:input-ref %}])
@ -78,39 +77,30 @@
:blur-on-submit false
:on-focus #(dispatch [:set-chat-ui-props {:input-focused? true
:show-emoji? false}])
:on-blur #(do (dispatch [:set-chat-ui-props {:input-focused? false}]))
:on-blur #(dispatch [:set-chat-ui-props {:input-focused? false}])
:on-submit-editing (fn [e]
(if single-line-input?
(dispatch [:send-current-message])
(.setNativeProps @input-ref (clj->js {:text (str @input-text "\n")}))))
:on-layout (fn [e]
(set-container-width (.-width (.-layout (.-nativeEvent e)))))
(set-container-width-fn (.-width (.-layout (.-nativeEvent e)))))
:on-change (fn [e]
(let [native-event (.-nativeEvent e)
text (.-text native-event)]
text (.-text native-event)
height (.. native-event -contentSize -height)]
(when-not single-line-input?
(let [height (.. native-event -contentSize -height)]
(set-layout-height height)))
(set-layout-height-fn height))
(when (not= text @input-text)
(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
:prev-command (-> @command :command :name)}]))))))
(when @command
(dispatch [:load-chat-parameter-box (:command @command)]))
(dispatch [:update-input-data]))))
:on-content-size-change (when (and (not @input-focused?)
(not single-line-input?))
#(let [h (-> (.-nativeEvent %)
(.-contentSize)
(.-height))]
(set-layout-height h)))
(set-layout-height-fn h)))
:on-selection-change #(let [s (-> (.-nativeEvent %)
(.-selection))
end (.-end s)]
@ -119,13 +109,13 @@
:placeholder-text-color style/color-input-helper-placeholder
:auto-capitalize :sentences}])))
(defn- invisible-input [{:keys [set-layout-width value]}]
(defn- invisible-input [{:keys [set-layout-width-fn value]}]
(let [input-text (subscribe [:chat :input-text])]
[text {:style style/invisible-input-text
:on-layout #(let [w (-> (.-nativeEvent %)
(.-layout)
(.-width))]
(set-layout-width w))}
(set-layout-width-fn w))}
(or @input-text "")]))
(defn- input-helper [_]
@ -183,25 +173,25 @@
(get-options type))])))))
(defn input-view [_]
(let [component (r/current-component)
set-layout-width #(r/set-state component {:width %})
set-layout-height #(r/set-state component {:height %})
set-container-width #(r/set-state component {:container-width %})
command (subscribe [:selected-chat-command])]
(let [component (r/current-component)
set-layout-width-fn #(r/set-state component {:width %})
set-layout-height-fn #(r/set-state component {:height %})
set-container-width-fn #(r/set-state component {:container-width %})
command (subscribe [:selected-chat-command])]
(r/create-class
{:reagent-render
(fn [{:keys [anim-margin single-line-input?]}]
(let [{:keys [width height container-width]} (r/state component)
command @command]
[animated-view {:style (style/input-root height anim-margin)}
[invisible-input {:set-layout-width set-layout-width}]
[basic-text-input {:set-layout-height set-layout-height
:set-container-width set-container-width
:height height
:single-line-input? single-line-input?}]
[invisible-input {:set-layout-width-fn set-layout-width-fn}]
[basic-text-input {:set-layout-height-fn set-layout-height-fn
:set-container-width-fn set-container-width-fn
:height height
:single-line-input? single-line-input?}]
[input-helper {:command command
:width width}]
[seq-input {:command-width width
[seq-input {:command-width width
:container-width container-width}]
(if-not command
[touchable-highlight

View File

@ -5,9 +5,9 @@
[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.chat.views.api.choose-contact :as choose-contact]
[status-im.components.qr-code :as qr]
[status-im.chat.views.geolocation.views :as geolocation]
[status-im.chat.views.api.geolocation.views :as geolocation]
[status-im.utils.handlers :refer [register-handler]]
[taoensso.timbre :as log]))

View File

@ -170,3 +170,7 @@
[keyboard-avoiding-view-class (merge {:behavior :padding} props)]
[view props])]
(vec (concat view-element children))))
;; Emoji
(def emojilib (js/require "emojilib"))