From ecc41bebe07b42a04aa5d8841ad70aa02bbf876b Mon Sep 17 00:00:00 2001 From: shivekkhurana Date: Thu, 19 Nov 2020 21:41:53 +0530 Subject: [PATCH] Moved text-input props to their own functions, renamed get-suggestions to get-user-suggestions Signed-off-by: shivekkhurana --- src/status_im/chat/models/mentions.cljs | 92 ++++----- src/status_im/chat/models/mentions_test.cljs | 25 ++- .../ui/screens/chat/components/input.cljs | 192 +++++++++--------- 3 files changed, 155 insertions(+), 154 deletions(-) diff --git a/src/status_im/chat/models/mentions.cljs b/src/status_im/chat/models/mentions.cljs index 687d562502..704df193df 100644 --- a/src/status_im/chat/models/mentions.cljs +++ b/src/status_im/chat/models/mentions.cljs @@ -231,7 +231,7 @@ lcase-text (string/lower-case text)] (re-find regex lcase-text))) -(defn get-suggestions [users searched-text] +(defn get-user-suggestions [users searched-text] (reduce (fn [acc [k {:keys [alias name nickname searchable-phrases] :as user}]] (if-let [match @@ -272,26 +272,26 @@ (match-mention text users mention-key-idx (inc mention-key-idx) [])) ([text users mention-key-idx next-word-idx words] (when-let [word (re-find word-regex (subs text next-word-idx))] - (let [new-words (conj words word) - searched-text (let [text (-> new-words - string/join - string/lower-case - string/trim) - last-char (dec (count text))] - (if (re-matches ending-chars-regex (str (nth text last-char nil))) - (subs text 0 last-char) - text)) - suggestions (get-suggestions users searched-text) - suggestions-cnt (count suggestions)] - (cond (zero? suggestions-cnt) + (let [new-words (conj words word) + searched-text (let [text (-> new-words + string/join + string/lower-case + string/trim) + last-char (dec (count text))] + (if (re-matches ending-chars-regex (str (nth text last-char nil))) + (subs text 0 last-char) + text)) + user-suggestions (get-user-suggestions users searched-text) + user-suggestions-cnt (count user-suggestions)] + (cond (zero? user-suggestions-cnt) nil - (and (= 1 suggestions-cnt) - (mentioned? (second (first suggestions)) + (and (= 1 user-suggestions-cnt) + (mentioned? (second (first user-suggestions)) (subs text (inc mention-key-idx)))) - (second (first suggestions)) + (second (first user-suggestions)) - (> suggestions-cnt 1) + (> user-suggestions-cnt 1) (let [word-len (count word) text-len (count text) next-word-start (+ next-word-idx word-len)] @@ -300,33 +300,32 @@ next-word-start new-words)))))))) (defn replace-mentions - ([text users-fn] + ([text users] (let [idxs (get-at-signs text)] - (replace-mentions text users-fn idxs 0))) - ([text users-fn idxs diff] + (replace-mentions text users idxs 0))) + ([text users idxs diff] (if (or (string/blank? text) (empty? idxs)) text (let [mention-key-idx (- (first idxs) diff)] (if-not mention-key-idx text - (let [users (users-fn)] - (if-not (seq users) - text - (let [{:keys [public-key match]} - (match-mention text users mention-key-idx)] - (if-not match - (recur text (fn [] users) (rest idxs) diff) - (let [new-text (string/join - [(subs text 0 (inc mention-key-idx)) - public-key - (subs text (+ (inc mention-key-idx) - (count match)))])] - (recur new-text (fn [] users) (rest idxs) - (+ diff (- (count text) (count new-text)))))))))))))) + (if-not (seq users) + text + (let [{:keys [public-key match]} + (match-mention text users mention-key-idx)] + (if-not match + (recur text users (rest idxs) diff) + (let [new-text (string/join + [(subs text 0 (inc mention-key-idx)) + public-key + (subs text (+ (inc mention-key-idx) + (count match)))])] + (recur new-text users (rest idxs) + (+ diff (- (count text) (count new-text))))))))))))) (defn check-mentions [cofx text] - (replace-mentions text #(get-mentionable-users cofx))) + (replace-mentions text (get-mentionable-users cofx))) (defn get-at-sign-idxs ([text start] @@ -401,14 +400,14 @@ new-idxs))))))) (defn check-entry - [text {:keys [from checked?] :as entry} users-fn] + [text {:keys [from checked?] :as entry} mentionable-users] (if checked? entry - (let [{:keys [match]} - (match-mention (str text "@") (users-fn) from)] - (if match + (let [{user-match :match} + (match-mention (str text "@") mentionable-users from)] + (if user-match {:from from - :to (+ from (count match)) + :to (+ from (count user-match)) :checked? true :mention? true} {:from from @@ -416,12 +415,13 @@ :checked? true :mention false})))) -(defn check-idx-for-mentions [text idxs users-fn] +(defn check-idx-for-mentions + [text idxs mentionable-users] (let [idxs (reduce (fn [acc {:keys [from] :as entry}] (let [previous-entry-idx (dec (count acc)) - new-entry (check-entry text entry users-fn)] + new-entry (check-entry text entry mentionable-users)] (cond-> acc (and (>= previous-entry-idx 0) (not (get-in acc [previous-entry-idx :mention?]))) @@ -497,7 +497,7 @@ new-at-idxs (check-idx-for-mentions text (:at-idxs state) - (fn [] mentionable-users)) + mentionable-users) calculated-input (calculate-input text new-at-idxs)] (log/debug "[mentions] new-at-idxs" new-at-idxs calculated-input) {:db (-> db @@ -524,8 +524,8 @@ (assoc-in [:chats/input-with-mentions chat-id] [[:text text]]))} (let [new-at-idxs (check-idx-for-mentions text - (:at-idxs state) - (fn [] mentionable-users)) + at-idxs + mentionable-users) calculated-input (calculate-input text new-at-idxs) addition? (<= start end) end (if addition? @@ -536,7 +536,7 @@ mentions (when (and (not (> at-sign-idx start)) (not (> (- end at-sign-idx) 100))) - (get-suggestions mentionable-users searched-text))] + (get-user-suggestions mentionable-users searched-text))] (log/debug "[mentions] mention check" "addition" addition? "at-sign-idx" at-sign-idx diff --git a/src/status_im/chat/models/mentions_test.cljs b/src/status_im/chat/models/mentions_test.cljs index 430bde6eda..a0cccadc60 100644 --- a/src/status_im/chat/models/mentions_test.cljs +++ b/src/status_im/chat/models/mentions_test.cljs @@ -4,19 +4,18 @@ [cljs.test :as test :include-macros true])) (test/deftest test-replace-mentions - (let [users (fn [] - {"User Number One" - {:name "User Number One" - :alias "User Number One" - :public-key "0xpk1"} - "User Number Two" - {:name "user2" - :alias "User Number Two" - :public-key "0xpk2"} - "User Number Three" - {:name "user3" - :alias "User Number Three" - :public-key "0xpk3"}})] + (let [users {"User Number One" + {:name "User Number One" + :alias "User Number One" + :public-key "0xpk1"} + "User Number Two" + {:name "user2" + :alias "User Number Two" + :public-key "0xpk2"} + "User Number Three" + {:name "user3" + :alias "User Number Three" + :public-key "0xpk3"}}] (test/testing "empty string" (let [text "" result (mentions/replace-mentions text users)] diff --git a/src/status_im/ui/screens/chat/components/input.cljs b/src/status_im/ui/screens/chat/components/input.cljs index 1382034a64..0b68dbbdb6 100644 --- a/src/status_im/ui/screens/chat/components/input.cljs +++ b/src/status_im/ui/screens/chat/components/input.cljs @@ -79,6 +79,80 @@ :accessibility-label :send-message-button :color (styles/send-icon-color)}]]]) +(defn selection [cursor] + ;; NOTE(rasom): In case if mention is added on pressing suggestion and + ;; it is placed inside some text we have to specify `:selection` on + ;; Android to ensure that cursor is added after the mention, not after + ;; the last char in input. On iOS it works that way without this code + (when (and cursor platform/android?) + (clj->js {:start cursor :end cursor}))) + +(defn on-selection-change [cursor timeout-id last-text-change mentionable-users args] + (let [selection (.-selection ^js (.-nativeEvent ^js args)) + start (.-start selection) + end (.-end selection)] + ;; NOTE(rasom): on iOS we do not dispatch this event immediately + ;; because it is needed only in case if selection is changed without + ;; typing. Timeout might be canceled on `on-change`. + (when platform/ios? + (reset! + timeout-id + (utils.utils/set-timeout + #(re-frame/dispatch [::mentions/on-selection-change + {:start start + :end end} + mentionable-users]) + 50))) + ;; NOTE(rasom): on Android we dispatch event only in case if there + ;; was no text changes during last 50ms. `on-selection-change` is + ;; dispatched after `on-change`, that's why there is no another way + ;; to know whether selection was changed without typing. + (when (and platform/android? + (or (not @last-text-change) + (< 50 (- (js/Date.now) @last-text-change)))) + (re-frame/dispatch [::mentions/on-selection-change + {:start start + :end end} + mentionable-users])) + ;; NOTE(rasom): we have to reset `cursor` value when user starts using + ;; text-input because otherwise cursor will stay in the same position + (when (and cursor platform/android?) + (re-frame/dispatch [::mentions/clear-cursor])))) + +(defn on-change [on-text-change last-text-change timeout-id mentionable-users args] + (let [text (.-text ^js (.-nativeEvent ^js args))] + ;; NOTE(rasom): on iOS `on-selection-change` is canceled in case if it + ;; happens during typing because it is not needed for mention + ;; suggestions calculation + (when (and platform/ios? @timeout-id) + (utils.utils/clear-timeout @timeout-id)) + (when platform/android? + (reset! last-text-change (js/Date.now))) + (on-text-change text) + ;; NOTE(rasom): on iOS `on-change` is dispatched after `on-text-input`, + ;; that's why mention suggestions are calculated on `on-change` + (when platform/ios? + (re-frame/dispatch [::mentions/calculate-suggestions mentionable-users])))) + +(defn on-text-input [mentionable-users args] + (let [native-event (.-nativeEvent ^js args) + text (.-text ^js native-event) + previous-text (.-previousText ^js native-event) + range (.-range ^js native-event) + start (.-start ^js range) + end (.-end ^js range)] + (re-frame/dispatch + [::mentions/on-text-input + {:new-text text + :previous-text previous-text + :start start + :end end}]) + ;; NOTE(rasom): on Android `on-text-input` is dispatched after + ;; `on-change`, that's why mention suggestions are calculated + ;; on `on-change` + (when platform/android? + (re-frame/dispatch [::mentions/calculate-suggestions mentionable-users])))) + (defn text-input [{:keys [cooldown-enabled? input-with-mentions on-text-change set-active-panel text-input-ref]}] (let [cursor @(re-frame/subscribe [:chat/cursor]) @@ -87,101 +161,29 @@ last-text-change (atom nil)] [rn/view {:style (styles/text-input-wrapper)} [rn/text-input - {:style (styles/text-input) - :ref text-input-ref - :maxFontSizeMultiplier 1 - :accessibility-label :chat-message-input - :text-align-vertical :center - :multiline true - :editable (not cooldown-enabled?) - :blur-on-submit false - :auto-focus false - :on-focus #(set-active-panel nil) - :max-length chat.constants/max-text-size - :placeholder-text-color (:text-02 @colors/theme) - :placeholder (if cooldown-enabled? - (i18n/label :cooldown/text-input-disabled) - (i18n/label :t/type-a-message)) - :underlineColorAndroid :transparent - :auto-capitalize :sentences - :selection - ;; NOTE(rasom): In case if mention is added on pressing suggestion and - ;; it is placed inside some text we have to specify `:selection` on - ;; Android to ensure that cursor is added after the mention, not after - ;; the last char in input. On iOS it works that way without this code - (when (and cursor platform/android?) - (clj->js - {:start cursor - :end cursor})) - - :on-selection-change - (fn [args] - (let [selection (.-selection ^js (.-nativeEvent ^js args)) - start (.-start selection) - end (.-end selection)] - ;; NOTE(rasom): on iOS we do not dispatch this event immediately - ;; because it is needed only in case if selection is changed without - ;; typing. Timeout might be canceled on `on-change`. - (when platform/ios? - (reset! - timeout-id - (utils.utils/set-timeout - #(re-frame/dispatch [::mentions/on-selection-change - {:start start - :end end} - mentionable-users]) - 50))) - ;; NOTE(rasom): on Android we dispatch event only in case if there - ;; was no text changes during last 50ms. `on-selection-change` is - ;; dispatched after `on-change`, that's why there is no another way - ;; to know whether selection was changed without typing. - (when (and platform/android? - (or (not @last-text-change) - (< 50 (- (js/Date.now) @last-text-change)))) - (re-frame/dispatch [::mentions/on-selection-change - {:start start - :end end} - mentionable-users])) - ;; NOTE(rasom): we have to reset `cursor` value when user starts using - ;; text-input because otherwise cursor will stay in the same position - (when (and cursor platform/android?) - (re-frame/dispatch [::mentions/clear-cursor])))) - - :on-change - (fn [args] - (let [text (.-text ^js (.-nativeEvent ^js args))] - ;; NOTE(rasom): on iOS `on-selection-change` is canceled in case if it - ;; happens during typing because it is not needed for mention - ;; suggestions calculation - (when (and platform/ios? @timeout-id) - (utils.utils/clear-timeout @timeout-id)) - (when platform/android? - (reset! last-text-change (js/Date.now))) - (on-text-change text) - ;; NOTE(rasom): on iOS `on-change` is dispatched after `on-text-input`, - ;; that's why mention suggestions are calculated on `on-change` - (when platform/ios? - (re-frame/dispatch [::mentions/calculate-suggestions mentionable-users])))) - - :on-text-input - (fn [args] - (let [native-event (.-nativeEvent ^js args) - text (.-text ^js native-event) - previous-text (.-previousText ^js native-event) - range (.-range ^js native-event) - start (.-start ^js range) - end (.-end ^js range)] - (re-frame/dispatch - [::mentions/on-text-input - {:new-text text - :previous-text previous-text - :start start - :end end}]) - ;; NOTE(rasom): on Android `on-text-input` is dispatched after - ;; `on-change`, that's why mention suggestions are calculated - ;; on `on-change` - (when platform/android? - (re-frame/dispatch [::mentions/calculate-suggestions mentionable-users]))))} + {:style (styles/text-input) + :ref text-input-ref + :max-font-size-multiplier 1 + :accessibility-label :chat-message-input + :text-align-vertical :center + :multiline true + :editable (not cooldown-enabled?) + :blur-on-submit false + :auto-focus false + :on-focus #(set-active-panel nil) + :max-length chat.constants/max-text-size + :placeholder-text-color (:text-02 @colors/theme) + :placeholder (if cooldown-enabled? + (i18n/label :cooldown/text-input-disabled) + (i18n/label :t/type-a-message)) + :underline-color-android :transparent + :auto-capitalize :sentences + :selection (selection cursor) + :on-selection-change (partial on-selection-change + cursor timeout-id last-text-change mentionable-users) + :on-change (partial on-change + on-text-change last-text-change timeout-id mentionable-users) + :on-text-input (partial on-text-input mentionable-users)} ;; NOTE(rasom): reduce was used instead of for here because although ;; each text component was given a unique id it still would mess with ;; colors on Android. In case if entire component is built without lists