Highlighted mentions in input

This commit is contained in:
Roman Volosovskyi 2020-09-21 13:10:49 +03:00
parent 44e3076022
commit 0448edb509
No known key found for this signature in database
GPG Key ID: 0238A4B5ECEE70DE
4 changed files with 221 additions and 13 deletions

View File

@ -30,7 +30,7 @@
{:db (assoc-in db [:chat/inputs current-chat-id :input-text] (text->emoji new-input))})
(fx/defn select-mention
[{:keys [db] :as cofx} {:keys [name] :as user}]
[{:keys [db] :as cofx} {:keys [alias name searched-text match] :as user}]
(let [chat-id (:current-chat-id db)
new-text (mentions/new-input-text-with-mention cofx user)
at-sign-idx (get-in db [:chats chat-id :mentions :at-sign-idx])]
@ -39,7 +39,17 @@
{:db (-> db
(assoc-in [:chats/cursor chat-id] (+ at-sign-idx (count name) 2))
(assoc-in [:chats/mention-suggestions chat-id] nil))}
(set-chat-input-text new-text))))
(set-chat-input-text new-text)
;; NOTE(roman): on-text-input event is not dispatched when we change input
;; programmatically, so we have to call `on-text-input` manually
(mentions/on-text-input
(let [match-len (count match)
searched-text-len (count searched-text)]
{:new-text (subs match searched-text-len)
:previous-text ""
:start (+ at-sign-idx searched-text-len 1)
:end (+ at-sign-idx match-len 1)}))
(mentions/recheck-at-idxs {alias user}))))
(defn- start-cooldown [{:keys [db]} cooldowns]
{:dispatch-later [{:dispatch [:chat/disable-cooldown]

View File

@ -85,7 +85,9 @@
(string/lower-case name)
searched-text)
name)]
(assoc acc k (assoc user :match match))
(assoc acc k (assoc user
:match match
:searched-text searched-text))
acc))
{}
users))
@ -169,6 +171,120 @@
(- (check-for-at-sign new-text)
(check-for-at-sign previous-text))))
(defn get-at-sign-idxs
([text start]
(get-at-sign-idxs text start 0 []))
([text start from idxs]
(if-let [idx (string/index-of text at-sign from)]
(recur text start (inc idx) (conj idxs (+ start idx)))
idxs)))
(defn calc-at-idxs
[{:keys [at-idxs new-text previous-text start]}]
(let [new-idxs (get-at-sign-idxs new-text start)
new-idx-cnt (count new-idxs)
last-new-idx (when (pos? new-idx-cnt)
(nth new-idxs (dec new-idx-cnt)))
new-text-len (count new-text)
old-text-len (count previous-text)
old-end (+ start old-text-len)]
(if-not (seq at-idxs)
(map (fn [idx]
{:from idx
:checked? false})
new-idxs)
(let [diff (- new-text-len old-text-len)
{:keys [state added?]}
(->> at-idxs
(keep (fn [{:keys [from to] :as entry}]
(let [to+1 (inc to)]
(cond
;; starts after change
(>= from old-end)
(assoc entry
:from (+ from diff)
:to (+ to diff))
;; starts and end before change
(and
(< from start)
(or
;; is not checked yet
(not to+1)
(< to+1 start)))
entry
;; starts before change intersects with it
(and (< from start)
(>= to+1 start))
{:from from
:checked? false}
;; starts in changed part of text
:else nil))))
(reduce
(fn [{:keys [state added?] :as acc} {:keys [from] :as entry}]
(if (and last-new-idx
(> from last-new-idx)
(not added?))
{:state (conj
(into state (map (fn [idx]
{:from idx
:checked? false})
new-idxs))
entry)
:added? true}
(update acc :state conj entry)))
{:state []}))]
(if added?
state
(into state (map (fn [idx]
{:from idx
:checked? false})
new-idxs)))))))
(defn check-entry
[text {:keys [from checked?] :as entry} users-fn]
(if checked?
entry
(let [{:keys [match]}
(match-mention (str text "@") (users-fn) from)]
(if match
{:from from
:to (+ from (count match))
:checked? true
:mention? true}
{:from from
:to (count text)
:checked? true
:mention false}))))
(defn check-idx-for-mentions [text idxs users-fn]
(let [idxs
(reduce
(fn [acc {:keys [from] :as entry}]
(let [previous-entry-idx (dec (count acc))
new-entry (check-entry text entry users-fn)]
(cond-> acc
(and (>= previous-entry-idx 0)
(not (get-in acc [previous-entry-idx :mention?])))
(assoc-in [previous-entry-idx :to] (dec from))
(>= previous-entry-idx 0)
(assoc-in [previous-entry-idx :next-at-idx] from)
:always
(conj (dissoc new-entry :next-at-idx)))))
[]
idxs)]
(when (seq idxs)
(let [last-idx (dec (count idxs))]
(if (get-in idxs [last-idx :mention?])
idxs
(-> idxs
(assoc-in [last-idx :to] (dec (count text)))
(assoc-in [last-idx :checked?] false)))))))
(fx/defn on-text-input
{:events [::on-text-input]}
[{:keys [db] :as cofx} {:keys [new-text previous-text start end] :as args}]
@ -182,13 +298,60 @@
chat-id (:current-chat-id db)
change (at-sign-change normalized-previous-text new-text)
previous-state (get-in db [:chats chat-id :mentions])
text (get-in db [:chat/inputs chat-id :input-text])
new-state (-> previous-state
(update :at-sign-counter + change)
(merge args)
(assoc :previous-text normalized-previous-text))]
(assoc :previous-text normalized-previous-text))
old-at-idxs (:at-idxs new-state)
new-at-idxs (calc-at-idxs new-state)
new-state (assoc new-state :at-idxs new-at-idxs)]
(log/debug "[mentions] on-text-input state" new-state)
{:db (assoc-in db [:chats chat-id :mentions] new-state)}))
(defn calculate-input [text [first-idx :as idxs]]
(if-not first-idx
[[:text text]]
(let [idx-cnt (count idxs)
last-from (get-in idxs [(dec idx-cnt) :from])]
(reduce
(fn [acc {:keys [from to next-at-idx mention?]}]
(cond
(and mention? next-at-idx)
(into acc [[:mention (subs text from (inc to))]
[:text (subs text (inc to) next-at-idx)]])
(and mention? (= last-from from))
(into acc [[:mention (subs text from (inc to))]
[:text (subs text (inc to))]])
:else
(conj acc [:text (subs text from (inc to))])))
(let [first-from (:from first-idx)]
(if (zero? first-from)
[]
[[:text (subs text 0 first-from)]]))
idxs))))
(fx/defn recheck-at-idxs
[{:keys [db]} mentionable-users]
(let [chat-id (:current-chat-id db)
text (get-in db [:chat/inputs chat-id :input-text])
{:keys [new-text at-sign-counter start end] :as state}
(get-in db [:chats chat-id :mentions])
new-at-idxs (check-idx-for-mentions
text
(:at-idxs state)
(fn [] mentionable-users))
calculated-input (calculate-input text new-at-idxs)]
(log/debug "[mentions] new-at-idxs" new-at-idxs calculated-input)
{:db (-> db
(update-in
[:chats chat-id :mentions]
assoc
:at-idxs new-at-idxs)
(assoc-in [:chats/input-with-mentions chat-id] calculated-input))}))
(fx/defn calculate-suggestion
{:events [::calculate-suggestions]}
[{:keys [db] :as cofx} mentionable-users]
@ -200,8 +363,16 @@
(log/debug "[mentions] calculate suggestions"
"state" state)
(if-not (pos? at-sign-counter)
{:db (assoc-in db [:chats/mention-suggestions chat-id] nil)}
(let [addition? (<= start end)
{:db (-> db
(assoc-in [:chats/mention-suggestions chat-id] nil)
(assoc-in [:chats chat-id :mentions :at-idxs] nil)
(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))
calculated-input (calculate-input text new-at-idxs)
addition? (<= start end)
end (if addition?
(+ start (count new-text))
start)
@ -216,21 +387,24 @@
"end" end
"searched-text" (pr-str searched-text)
"mentions" (count mentions))
(log/debug "[mentions] new-at-idxs" new-at-idxs calculated-input)
{:db (-> db
(update-in [:chats chat-id :mentions]
assoc
:at-sign-idx at-sign-idx
:at-idxs new-at-idxs
:mention-end end)
(assoc-in [:chats/input-with-mentions chat-id] calculated-input)
(assoc-in [:chats/mention-suggestions chat-id] mentions))}))))
(defn new-input-text-with-mention
[{:keys [db]} {:keys [name]}]
(let [chat-id (:current-chat-id db)
text (get-in db [:chat/inputs chat-id :input-text])
{:keys [mention-end at-sign-idx] :as state}
{:keys [mention-end at-sign-idx]}
(get-in db [:chats chat-id :mentions])]
(log/debug "[mentions] clear suggestions"
"state" state)
"state" new-input-text-with-mention)
(string/join
[(subs text 0 (inc at-sign-idx))
name
@ -247,6 +421,7 @@
(let [chat-id (:current-chat-id db)]
{:db (-> db
(update-in [:chats chat-id] dissoc :mentions)
(update :chats/input-with-mentions dissoc chat-id)
(update :chats/mention-suggestions dissoc chat-id))}))
(fx/defn clear-cursor

View File

@ -121,7 +121,7 @@
(reg-root-key-sub :group-chat/invitations :group-chat/invitations)
(reg-root-key-sub :chats/mention-suggestions :chats/mention-suggestions)
(reg-root-key-sub :chats/cursor :chats/cursor)
(reg-root-key-sub :chats/input-with-mentions :chats/input-with-mentions)
;;browser
(reg-root-key-sub :browsers :browser/browsers)
(reg-root-key-sub :browser/options :browser/options)
@ -904,6 +904,13 @@
(fn [[chat-id cursor]]
(get cursor chat-id)))
(re-frame/reg-sub
:chat/input-with-mentions
:<- [:chats/current-chat-id]
:<- [:chats/input-with-mentions]
(fn [[chat-id cursor]]
(get cursor chat-id)))
;;BOOTNODES ============================================================================================================
(re-frame/reg-sub

View File

@ -79,7 +79,7 @@
:color (styles/send-icon-color)}]]])
(defn text-input
[{:keys [cooldown-enabled? text-value on-text-change set-active-panel text-input-ref]}]
[{:keys [cooldown-enabled? input-with-mentions on-text-change set-active-panel text-input-ref]}]
(let [cursor @(re-frame/subscribe [:chat/cursor])
mentionable-users @(re-frame/subscribe [:chats/mentionable-users])]
[rn/view {:style (styles/text-input-wrapper)}
@ -90,7 +90,6 @@
:accessibility-label :chat-message-input
:text-align-vertical :center
:multiline true
:default-value text-value
:editable (not cooldown-enabled?)
:blur-on-submit false
:auto-focus false
@ -146,7 +145,20 @@
;; `on-change`, that's why mention suggestions are calculated
;; on `on-change`
(when platform/android?
(re-frame/dispatch [::mentions/calculate-suggestions mentionable-users]))))}]]))
(re-frame/dispatch [::mentions/calculate-suggestions 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
;; inside it works just fine on both platforms.
(reduce
(fn [acc [type text]]
(conj
acc
[rn/text (when (= type :mention)
{:style {:color "#0DA4C9"}})
text]))
[:<>]
input-with-mentions)]]))
(defn mention-item
[[_ {:keys [identicon alias name] :as user}]]
@ -248,7 +260,10 @@
(let [disconnected? @(re-frame/subscribe [:disconnected?])
{:keys [processing]} @(re-frame/subscribe [:multiaccounts/login])
mainnet? @(re-frame/subscribe [:mainnet?])
input-text @(re-frame/subscribe [:chats/current-chat-input-text])
input-text
@(re-frame/subscribe [:chats/current-chat-input-text])
input-with-mentions
@(re-frame/subscribe [:chat/input-with-mentions])
cooldown-enabled? @(re-frame/subscribe [:chats/cooldown-enabled?])
one-to-one-chat? @(re-frame/subscribe [:current-chat/one-to-one-chat?])
{:keys [public?
@ -295,6 +310,7 @@
:on-send-press #(do (re-frame/dispatch [:chat.ui/send-current-message])
(clear-input))
:text-value input-text
:input-with-mentions input-with-mentions
:on-text-change on-text-change
:cooldown-enabled? cooldown-enabled?
:show-send show-send