Plug-in new text parsing engine

This commit is contained in:
janherich 2018-10-18 02:05:39 +02:00
parent 8d168bf3cd
commit ff97345f07
No known key found for this signature in database
GPG Key ID: C23B473AFBE94D13
16 changed files with 228 additions and 309 deletions

View File

@ -3,7 +3,7 @@
[clojure.set :as set] [clojure.set :as set]
[pluto.reader.hooks :as hooks] [pluto.reader.hooks :as hooks]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.chat.constants :as chat-constants] [status-im.chat.constants :as chat.constants]
[status-im.chat.commands.protocol :as protocol] [status-im.chat.commands.protocol :as protocol]
[status-im.chat.commands.impl.transactions :as transactions] [status-im.chat.commands.impl.transactions :as transactions]
[status-im.utils.handlers :as handlers] [status-im.utils.handlers :as handlers]
@ -22,7 +22,7 @@
"Given the command instance, returns command name as displayed in chat input, "Given the command instance, returns command name as displayed in chat input,
with leading `/` character." with leading `/` character."
[type] [type]
(str chat-constants/command-char (protocol/id type))) (str chat.constants/command-char (protocol/id type)))
(defn command-description (defn command-description
"Returns description for the command." "Returns description for the command."

View File

@ -36,11 +36,18 @@
:message message}))) :message message})))
(defn- prepare-message (defn- prepare-message
[{:keys [content] :as message} chat-id current-chat?] [{:keys [content content-type] :as message} chat-id current-chat?]
(let [emoji? (message-content/emoji-only-content? content)]
;; TODO janherich: enable the animations again once we can do them more efficiently ;; TODO janherich: enable the animations again once we can do them more efficiently
(cond-> (assoc message :appearing? true) (cond-> (assoc message :appearing? true)
(not current-chat?) (assoc :appearing? false) (not current-chat?)
(message-content/emoji-only-content? content) (assoc :content-type constants/content-type-emoji))) (assoc :appearing? false)
emoji?
(assoc :content-type constants/content-type-emoji)
(and (= constants/content-type-text content-type) (not emoji?))
(update :content message-content/enrich-content))))
(fx/defn re-index-message-groups (fx/defn re-index-message-groups
"Relative datemarks of message groups can get obsolete with passing time, "Relative datemarks of message groups can get obsolete with passing time,
@ -326,6 +333,10 @@
(add-message-type chat))] (add-message-type chat))]
(upsert-and-send cofx message-data))) (upsert-and-send cofx message-data)))
(fx/defn toggle-expand-message
[{:keys [db]} chat-id message-id]
{:db (update-in db [:chats chat-id :messages message-id :expanded?] not)})
;; effects ;; effects
(re-frame.core/reg-fx (re-frame.core/reg-fx

View File

@ -2,109 +2,103 @@
(:require [clojure.string :as string] (:require [clojure.string :as string]
[status-im.constants :as constants])) [status-im.constants :as constants]))
(def ^:private actions {:link constants/regx-url (def stylings [[:bold constants/regx-bold]
:tag constants/regx-tag [:italic constants/regx-italic]
:mention constants/regx-mention}) [:backquote constants/regx-backquote]])
(def ^:private stylings {:bold constants/regx-bold (def styling-keys (into #{} (map first) stylings))
:italic constants/regx-italic})
(def ^:private styling-characters #"\*|~") (def ^:private actions [[:link constants/regx-url]
[:tag constants/regx-tag]
[:mention constants/regx-mention]])
(def ^:private type->regex (merge actions stylings)) (defn- blank-string [size]
(.repeat " " size))
(defn- right-to-left-text? [text] (defn- clear-ranges [ranges input]
(and (seq text) (reduce (fn [acc [start end]]
(re-matches constants/regx-rtl-characters (first text)))) (.concat (subs acc 0 start) (blank-string (- end start)) (subs acc end)))
input ranges))
(defn- query-regex [regex content] (defn- query-regex [regex content]
(loop [input content (loop [input content
matches [] matches []
offset 0] offset 0]
(if-let [match (.exec regex input)] (if-let [match (.exec regex input)]
(let [match-value (aget match 0) (let [match-value (first match)
match-size (count match-value)
relative-index (.-index match) relative-index (.-index match)
start-index (+ offset relative-index) start-index (+ offset relative-index)
end-index (+ start-index (count match-value))] end-index (+ start-index match-size)]
(recur (apply str (drop end-index input)) (recur (subs input (+ relative-index match-size))
(conj matches [start-index end-index]) (conj matches [start-index end-index])
end-index)) end-index))
(seq matches)))) (seq matches))))
(defn- right-to-left-text? [text]
(and (seq text)
(re-matches constants/regx-rtl-characters (first text))))
(defn- should-collapse? [text]
(or (<= constants/chars-collapse-threshold (count text))
(<= constants/lines-collapse-threshold (inc (count (query-regex #"\n" text))))))
(defn- sorted-ranges [{:keys [metadata text]} metadata-keys]
(->> (if metadata-keys
(select-keys metadata metadata-keys)
metadata)
(reduce-kv (fn [acc type ranges]
(reduce #(assoc %1 %2 type) acc ranges))
{})
(sort-by ffirst)))
(defn build-render-recipe
"Builds render recipe from message text and metadata, can be used by render code
by simply iterating over it and paying attention to `:kind` set for each segment of text.
Optional in optional 2 arity version, you can pass collection of keys determining which
metadata to include in the render recipe (all of them by default)."
([content]
(build-render-recipe content nil))
([{:keys [text metadata] :as content} metadata-keys]
(when metadata
(let [[offset builder] (->> (sorted-ranges content metadata-keys)
(reduce (fn [[offset builder] [[start end] kind]]
(if (< start offset)
[offset builder] ;; next record is nested, not allowed, discard
(let [record-text (subs text start end)
record (if (styling-keys kind)
[(subs record-text 1
(dec (count record-text))) kind]
[record-text kind])]
(if-let [padding (when-not (= offset start)
[(subs text offset start) :text])]
[end (conj builder padding record)]
[end (conj builder record)]))))
[0 []]))
end-record (when-not (= offset (count text))
[(subs text offset (count text)) :text])]
(cond-> builder
end-record (conj end-record))))))
(defn enrich-content (defn enrich-content
"Enriches message content with `:metadata` and `:rtl?` information. "Enriches message content with `:metadata`, `:render-recipe` and `:rtl?` information.
Metadata map keys can by any of the `:link`, `:tag`, `:mention` actions Metadata map keys can by any of the `:link`, `:tag`, `:mention` actions
or `:bold` and `:italic` stylings. or `:bold` and `:italic` stylings.
Value for each key is sequence of tuples representing ranges in original Value for each key is sequence of tuples representing ranges in original
`:text` content. " `:text` content. "
[{:keys [text] :as content}] [{:keys [text] :as content}]
(let [metadata (reduce-kv (fn [metadata type regex] (let [[_ metadata] (reduce (fn [[text metadata] [type regex]]
(if-let [matches (query-regex regex text)] (if-let [matches (query-regex regex text)]
(assoc metadata type matches) [(clear-ranges matches text) (assoc metadata type matches)]
metadata)) [text metadata]))
{} [text {}]
type->regex)] (into stylings actions))]
(cond-> content (cond-> content
(seq metadata) (assoc :metadata metadata) (seq metadata) (as-> content
(right-to-left-text? text) (assoc :rtl? true)))) (assoc content :metadata metadata)
(assoc content :render-recipe (build-render-recipe content)))
(defn- sorted-ranges [{:keys [metadata text]}] (right-to-left-text? text) (assoc :rtl? true)
(->> metadata (should-collapse? text) (assoc :should-collapse? true))))
(reduce-kv (fn [acc type ranges]
(reduce #(assoc %1 %2 type) acc ranges))
{})
(sort-by (comp (juxt first second) first))
(cons [[0 (count text)] :text])))
(defn- last-index [result]
(or (some-> result peek :end) 0))
(defn- start [[[start]]] start)
(defn- end [[[_ end]]] end)
(defn- kind [[_ kind]] kind)
(defn- result-record [start end path]
{:start start
:end end
:kind (into #{} (map kind) path)})
(defn build-render-recipe
"Builds render recipe from message text and metadata, can be used by render code
by simply iterating over it and paying attention to `:kind` set for each segment of text."
[{:keys [text metadata] :as content}]
(letfn [(builder [[top :as stack] [input & rest-inputs :as inputs] result]
(if (seq input)
(cond
;; input is child of the top
(and (<= (start input) (end top))
(<= (end input) (end top)))
(recur (conj stack input) rest-inputs
(conj result (result-record (last-index result) (start input) stack)))
;; input overlaps top, it's neither child, nor sibling, discard input
(and (>= (start input) (start top))
(<= (start input) (end top)))
(recur stack rest-inputs result)
;; the only remaining possibility, input is next sibling to top
:else
(recur (rest stack) inputs
(conj result (result-record (last-index result) (end top) stack))))
;; inputs consumed, unwind stack
(loop [[top & rest-stack :as stack] stack
result result]
(if top
(recur rest-stack
(conj result (result-record (last-index result) (end top) stack)))
result))))]
(when metadata
(let [[head & tail] (sorted-ranges content)]
(->> (builder (list head) tail [])
(keep (fn [{:keys [start end kind]}]
(let [text-content (-> (subs text start end) ;; select text chunk & remove styling chars
(string/replace styling-characters ""))]
(when (seq text-content) ;; filter out empty text chunks
[text-content kind])))))))))
(defn emoji-only-content? (defn emoji-only-content?
"Determines if text is just an emoji" "Determines if text is just an emoji"

View File

@ -7,9 +7,9 @@
(def ethereum-rpc-url "http://localhost:8545") (def ethereum-rpc-url "http://localhost:8545")
(def content-type-text "text/plain") (def content-type-text "text/plain")
(def content-type-status "status")
(def content-type-command "command") (def content-type-command "command")
(def content-type-command-request "command-request") (def content-type-command-request "command-request")
(def content-type-status "status")
(def content-type-emoji "emoji") (def content-type-emoji "emoji")
(def desktop-content-types (def desktop-content-types
@ -211,6 +211,10 @@
(def regx-mention #"@[a-z0-9\-]+") (def regx-mention #"@[a-z0-9\-]+")
(def regx-bold #"\*[^*]+\*") (def regx-bold #"\*[^*]+\*")
(def regx-italic #"~[^~]+~") (def regx-italic #"~[^~]+~")
(def regx-backquote #"`[^`]+`")
(def ^:const lines-collapse-threshold 20)
(def ^:const chars-collapse-threshold 600)
(def ^:const dapp-permission-contact-code "contact-code") (def ^:const dapp-permission-contact-code "contact-code")
(def ^:const dapp-permission-web3 "web3") (def ^:const dapp-permission-web3 "web3")

View File

@ -197,6 +197,8 @@
browser/v8 browser/v8
dapp-permissions/v9]) dapp-permissions/v9])
(def v20 v19)
;; put schemas ordered by version ;; put schemas ordered by version
(def schemas [{:schema v1 (def schemas [{:schema v1
:schemaVersion 1 :schemaVersion 1
@ -254,4 +256,7 @@
:migration migrations/v18} :migration migrations/v18}
{:schema v19 {:schema v19
:schemaVersion 19 :schemaVersion 19
:migration migrations/v19}]) :migration migrations/v19}
{:schema v20
:schemaVersion 20
:migration migrations/v20}])

View File

@ -1,6 +1,7 @@
(ns status-im.data-store.realm.schemas.account.migrations (ns status-im.data-store.realm.schemas.account.migrations
(:require [taoensso.timbre :as log] (:require [taoensso.timbre :as log]
[cljs.reader :as reader])) [cljs.reader :as reader]
[status-im.chat.models.message-content :as message-content]))
(defn v1 [old-realm new-realm] (defn v1 [old-realm new-realm]
(log/debug "migrating v1 account database: " old-realm new-realm)) (log/debug "migrating v1 account database: " old-realm new-realm))
@ -116,3 +117,14 @@
(defn v19 [old-realm new-realm] (defn v19 [old-realm new-realm]
(log/debug "migrating v19 account database")) (log/debug "migrating v19 account database"))
(defn v20 [old-realm new-realm]
(log/debug "migrating v19 account database")
(some-> new-realm
(.objects "message")
(.filtered (str "content-type = \"text/plain\""))
(.map (fn [message _ _]
(let [content (reader/read-string (aget message "content"))
new-content (message-content/enrich-content content)]
(aset message "content" (pr-str new-content)))))))

View File

@ -587,6 +587,11 @@
(fn [cofx [_ chat-id message-id]] (fn [cofx [_ chat-id message-id]]
(chat.message/delete-message cofx chat-id message-id))) (chat.message/delete-message cofx chat-id message-id)))
(handlers/register-handler-fx
:chat.ui/message-expand-toggled
(fn [cofx [_ chat-id message-id]]
(chat.message/toggle-expand-message cofx chat-id message-id)))
(handlers/register-handler-fx (handlers/register-handler-fx
:chat.ui/show-profile :chat.ui/show-profile
(fn [cofx [_ identity]] (fn [cofx [_ identity]]

View File

@ -14,7 +14,7 @@
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.ui.components.chat-icon.screen :as chat-icon.screen] [status-im.ui.components.chat-icon.screen :as chat-icon.screen]
[status-im.utils.core :as utils] [status-im.utils.core :as utils]
[status-im.ui.screens.chat.utils :as chat-utils] [status-im.ui.screens.chat.utils :as chat.utils]
[status-im.utils.identicon :as identicon] [status-im.utils.identicon :as identicon]
[status-im.utils.gfycat.core :as gfycat] [status-im.utils.gfycat.core :as gfycat]
[status-im.utils.platform :as platform] [status-im.utils.platform :as platform]
@ -29,17 +29,9 @@
(commands/generate-preview command command-message) (commands/generate-preview command command-message)
[react/text (str "Unhandled command: " (-> command-message :content :command-path first))]))) [react/text (str "Unhandled command: " (-> command-message :content :command-path first))])))
(def rtl-characters-regex #"[^\u0591-\u06EF\u06FA-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]*?[\u0591-\u06EF\u06FA-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]")
(defn right-to-left-text? [content]
(when-not (empty? content)
(let [char (first content)]
(re-matches rtl-characters-regex char))))
(defview message-timestamp [t justify-timestamp? outgoing command? content] (defview message-timestamp [t justify-timestamp? outgoing command? content]
(when-not command? (when-not command?
(let [rtl? (right-to-left-text? (:text content))] [react/text {:style (style/message-timestamp-text justify-timestamp? outgoing (:rtl? content))} t]))
[react/text {:style (style/message-timestamp-text justify-timestamp? outgoing rtl?)} t])))
(defn message-view (defn message-view
[{:keys [timestamp-str outgoing content] :as message} message-content {:keys [justify-timestamp?]}] [{:keys [timestamp-str outgoing content] :as message} message-content {:keys [justify-timestamp?]}]
@ -49,111 +41,19 @@
(get content :command-ref)) (get content :command-ref))
content]]) content]])
(def replacements
{"\\*[^*]+\\*" {:font-weight :bold}
"~[^~]+~" {:font-style :italic}})
(def regx-styled (re-pattern (string/join "|" (map first replacements))))
(def regx-url #"(?i)(?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9\-]+[.][a-z]{1,4}/?)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'\".,<>?«»“”‘’]){0,}")
(defn- parse-str-regx [string regx matched-fn unmatched-fn]
(if (string? string)
(let [unmatched-text (as-> (->> (string/split string regx)
(remove nil?)
vec) $
(if (zero? (count $))
[nil]
(unmatched-fn $)))
matched-text (as-> (->> string
(re-seq regx)
matched-fn
vec) $
(if (> (count unmatched-text)
(count $))
(conj $ nil)
$))]
(mapcat vector unmatched-text matched-text))
(str string)))
(defn parse-url [string]
(parse-str-regx string
regx-url
(fn [text-seq]
(map (fn [[text]] {:text text :url? true}) text-seq))
(fn [text-seq]
(map (fn [text] {:text text :url? false}) text-seq))))
(defn- autolink [string event-on-press outgoing]
(->> (parse-url string)
(map-indexed (fn [idx {:keys [text url?]}]
(if url?
(let [[url _ _ _ text] (re-matches #"(?i)^((\w+://)?(www\d{0,3}[.])?)?(.*)$" text)]
[react/text
{:key idx
:style {:color (if outgoing colors/white colors/blue)
:text-decoration-line :underline}
:on-press #(re-frame/dispatch [event-on-press url])}
url])
text)))
vec))
(defn get-style [string]
(->> replacements
(into [] (comp (map first)
(map #(vector % (re-pattern %)))
(drop-while (fn [[_ regx]] (not (re-matches regx string))))
(take 1)))
ffirst
replacements))
;; todo rewrite this, naive implementation
(defn- parse-text [string event-on-press outgoing]
(parse-str-regx string
regx-styled
(fn [text-seq]
(map-indexed (fn [idx string]
(let [style (get-style string)]
[react/text
{:key (str idx "_" string)
:style style}
(subs string 1 (dec (count string)))]))
text-seq))
(fn [text-seq]
(map-indexed (fn [idx string]
(apply react/text
{:key (str idx "_" string)}
(autolink string event-on-press outgoing)))
text-seq))))
; We can't use CSS as nested Text element don't accept margins nor padding ; We can't use CSS as nested Text element don't accept margins nor padding
; so we pad the invisible placeholder with some spaces to avoid having too ; so we pad the invisible placeholder with some spaces to avoid having too
; close to the text. ; close to the text.
(defn timestamp-with-padding [t] (defn timestamp-with-padding [t]
(str " " t)) (str " " t))
(def cached-parse-text (memoize parse-text))
(def ^:private ^:const number-of-lines 20)
(def ^:private ^:const number-of-chars 600)
(defn- should-collapse? [text group-chat?]
(and group-chat?
(or (<= number-of-chars (count text))
(<= number-of-lines (count (re-seq #"\n" text))))))
(defn- expand-button [collapsed? on-press]
[react/text {:style style/message-expand-button
:on-press on-press}
(i18n/label (if @collapsed? :show-more :show-less))])
(defview quoted-message [{:keys [from text]} outgoing current-public-key] (defview quoted-message [{:keys [from text]} outgoing current-public-key]
(letsubs [username [:get-contact-name-by-identity from]] (letsubs [username [:get-contact-name-by-identity from]]
[react/view {:style (style/quoted-message-container outgoing)} [react/view {:style (style/quoted-message-container outgoing)}
[react/view {:style style/quoted-message-author-container} [react/view {:style style/quoted-message-author-container}
[vector-icons/icon :icons/reply {:color (if outgoing colors/wild-blue-yonder colors/gray)}] [vector-icons/icon :icons/reply {:color (if outgoing colors/wild-blue-yonder colors/gray)}]
[react/text {:style (style/quoted-message-author outgoing)} [react/text {:style (style/quoted-message-author outgoing)}
(chat-utils/format-reply-author from username current-public-key)]] (chat.utils/format-reply-author from username current-public-key)]]
[react/text {:style (style/quoted-message-text outgoing) [react/text {:style (style/quoted-message-text outgoing)
:number-of-lines 5} :number-of-lines 5}
text]])) text]]))
@ -162,32 +62,30 @@
[react/view style/status-container [react/view style/status-container
[react/text {:style style/status-text [react/text {:style style/status-text
:font :default} :font :default}
(cached-parse-text (:text content) nil false)]]) (:text content)]])
(defn- expand-button [expanded? chat-id message-id]
[react/text {:style style/message-expand-button
:on-press #(re-frame/dispatch [:chat.ui/message-expand-toggled chat-id message-id])}
(i18n/label (if expanded? :show-less :show-more))])
(defn text-message (defn text-message
[{:keys [content timestamp-str group-chat outgoing current-public-key] :as message}] [{:keys [chat-id message-id content timestamp-str group-chat outgoing current-public-key expanded?] :as message}]
[message-view message [message-view message
(let [parsed-text (cached-parse-text (:text content) :browser.ui/message-link-pressed outgoing) (let [collapsible? (and (:should-collapse? content) group-chat)]
ref (reagent/atom nil)
collapsible? (should-collapse? (:text content) group-chat)
collapsed? (reagent/atom collapsible?)
on-press (when collapsible?
#(do
(.setNativeProps @ref
(clj->js {:numberOfLines
(when-not @collapsed?
number-of-lines)}))
(reset! collapsed? (not @collapsed?))))]
[react/view [react/view
(when (:response-to content) (when (:response-to content)
[quoted-message (:response-to content) outgoing current-public-key]) [quoted-message (:response-to content) outgoing current-public-key])
[react/text {:style (style/text-message collapsible? outgoing) [react/text (cond-> {:style (style/text-message collapsible? outgoing)}
:number-of-lines (when collapsible? number-of-lines) (and collapsible? (not expanded?))
:ref (partial reset! ref)} (assoc :number-of-lines constants/lines-collapse-threshold))
parsed-text (if-let [render-recipe (:render-recipe content)]
[react/text {:style (style/message-timestamp-placeholder-text outgoing)} (timestamp-with-padding timestamp-str)]] (chat.utils/render-chunks render-recipe message)
(:text content))
[react/text {:style (style/message-timestamp-placeholder-text outgoing)}
(timestamp-with-padding timestamp-str)]]
(when collapsible? (when collapsible?
[expand-button collapsed? on-press])]) [expand-button expanded? chat-id message-id])])
{:justify-timestamp? true}]) {:justify-timestamp? true}])
(defn emoji-message (defn emoji-message
@ -314,7 +212,7 @@
(defview message-author-name [from message-username] (defview message-author-name [from message-username]
(letsubs [username [:get-contact-name-by-identity from]] (letsubs [username [:get-contact-name-by-identity from]]
[react/text {:style style/message-author-name} [react/text {:style style/message-author-name}
(chat-utils/format-author from (or username message-username))])) (chat.utils/format-author from (or username message-username))]))
(defn message-body (defn message-body
[{:keys [last-in-group? [{:keys [last-in-group?

View File

@ -58,7 +58,7 @@
{:color colors/gray {:color colors/gray
:font-size 12 :font-size 12
:opacity 0.7 :opacity 0.7
:margin-bottom 1}) :margin-bottom 20})
(def selected-message (def selected-message
{:margin-top 18 {:margin-top 18

View File

@ -1,6 +1,11 @@
(ns status-im.ui.screens.chat.utils (ns status-im.ui.screens.chat.utils
(:require [status-im.utils.gfycat.core :as gfycat] (:require [re-frame.core :as re-frame]
[status-im.i18n :as i18n])) [status-im.utils.gfycat.core :as gfycat]
[status-im.utils.platform :as platform]
[status-im.i18n :as i18n]
[status-im.ui.components.react :as react]
[status-im.ui.components.colors :as colors]
[status-im.utils.http :as http]))
(defn format-author [from username] (defn format-author [from username]
(str (when username (str username " :: ")) (str (when username (str username " :: "))
@ -10,3 +15,41 @@
(or (and (= from current-public-key) (i18n/label :t/You)) (or (and (= from current-public-key) (i18n/label :t/You))
(format-author from username))) (format-author from username)))
(def ^:private styling->prop
{:bold (if platform/desktop?
{:font :medium}
{:style {:font-weight :bold}})
:italic (if platform/desktop?
{:font :italic}
{:style {:font-style :italic}})
:backquote {:style {:background-color colors/black
:color colors/green}}})
(def ^:private action->prop-fn
{:link (fn [text {:keys [outgoing]}]
{:style {:color (if platform/desktop?
colors/blue
(if outgoing colors/white colors/blue))
:text-decoration-line :underline}
:on-press (if platform/desktop?
#(.openURL react/linking (http/normalize-url text))
#(re-frame/dispatch [:browser.ui/message-link-pressed text]))})
:tag (fn [text {:keys [outgoing]}]
{:style {:color (if platform/desktop?
colors/blue
(if outgoing colors/white colors/blue))
:text-decoration-line :underline}
:on-press #(re-frame/dispatch [:chat.ui/start-public-chat (subs text 1)])})})
(defn- lookup-props [text-chunk message kind]
(let [prop (get styling->prop kind)
prop-fn (get action->prop-fn kind)]
(if prop-fn (prop-fn text-chunk message) prop)))
(defn render-chunks [render-recipe message]
(map-indexed (fn [idx [text-chunk kind]]
(if (= :text kind)
text-chunk
[react/text (into {:key idx} (lookup-props text-chunk message kind))
text-chunk]))
render-recipe))

View File

@ -23,8 +23,7 @@
[status-im.utils.contacts :as utils.contacts] [status-im.utils.contacts :as utils.contacts]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.ui.screens.desktop.main.chat.events :as chat.events] [status-im.ui.screens.desktop.main.chat.events :as chat.events]
[status-im.ui.screens.chat.message.message :as chat.message] [status-im.ui.screens.chat.message.message :as chat.message]))
[status-im.utils.http :as http]))
(views/defview toolbar-chat-view [{:keys [chat-id color public-key public? group-chat] (views/defview toolbar-chat-view [{:keys [chat-id color public-key public? group-chat]
:as current-chat}] :as current-chat}]
@ -109,31 +108,11 @@
:number-of-lines 5} :number-of-lines 5}
text]])) text]]))
;; Include both URLs and channel links in regexp
(def regx-url #"(?i)((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9\-]+[.][a-z]{1,4}/?)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'\".,<>?«»“”‘’]){0,}|#[a-z0-9\-]+)")
(defn link-elem [link]
[react/text {:style (styles/message-link false)
:on-press #(if (string/starts-with? link "#")
(re-frame/dispatch [:chat.ui/start-public-chat (subs link 1)])
(.openURL react/linking (http/normalize-url link)))}
link])
(defn process-message-links [text]
;; JS and hence CLJS string/split will include delimiters
;; (urls and channel links in our case)
;; in the result array if they are inside a group
(->> (string/split text regx-url)
(remove nil?)
(map #(if (re-matches regx-url %1)
(link-elem %1)
%1))))
(defn- message-sent? [user-statuses current-public-key] (defn- message-sent? [user-statuses current-public-key]
(not= (get-in user-statuses [current-public-key :status]) :not-sent)) (not= (get-in user-statuses [current-public-key :status]) :not-sent))
(views/defview message-without-timestamp (views/defview message-without-timestamp
[text {:keys [message-id content current-public-key user-statuses]} style] [text {:keys [message-id content current-public-key user-statuses] :as message} style]
[react/view {:flex 1 :margin-vertical 5} [react/view {:flex 1 :margin-vertical 5}
[react/touchable-highlight {:on-press #(if (= "right" (.-button (.-nativeEvent %))) [react/touchable-highlight {:on-press #(if (= "right" (.-button (.-nativeEvent %)))
(do (utils/show-popup "" "Message copied to clipboard") (do (utils/show-popup "" "Message copied to clipboard")
@ -143,10 +122,12 @@
[react/view {:style styles/message-container} [react/view {:style styles/message-container}
(when (:response-to content) (when (:response-to content)
[quoted-message (:response-to content) false current-public-key]) [quoted-message (:response-to content) false current-public-key])
(into [react/text {:style (styles/message-text false) [react/text {:style (styles/message-text false)
:selectable true :selectable true
:selection-color colors/blue-light}] :selection-color colors/blue-light}
(process-message-links text))]]]) (if-let [render-recipe (:render-recipe content)]
(chat-utils/render-chunks render-recipe message)
(:text content))]]]])
(views/defview photo-placeholder [] (views/defview photo-placeholder []
[react/view {:style {:width 40 [react/view {:style {:width 40

View File

@ -24,7 +24,7 @@
unviewed-messages-count [:unviewed-messages-count chat-id] unviewed-messages-count [:unviewed-messages-count chat-id]
chat-name [:get-chat-name chat-id] chat-name [:get-chat-name chat-id]
current-chat-id [:get-current-chat-id] current-chat-id [:get-current-chat-id]
last-message [:get-last-message chat-id]] {:keys [content] :as last-message} [:get-last-message chat-id]]
(let [name (or chat-name (let [name (or chat-name
(gfycat/generate-gfy public-key)) (gfycat/generate-gfy public-key))
[unviewed-messages-label large?] (if (< 9 unviewed-messages-count) [unviewed-messages-label large?] (if (< 9 unviewed-messages-count)
@ -57,7 +57,8 @@
:style styles/chat-last-message} :style styles/chat-last-message}
(if (= constants/content-type-command (:content-type last-message)) (if (= constants/content-type-command (:content-type last-message))
[chat-item/command-short-preview last-message] [chat-item/command-short-preview last-message]
(or (get-in last-message [:content :text]) (i18n/label :no-messages-yet)))]] (or (:text content)
(i18n/label :no-messages-yet)))]]
[react/view {:style styles/timestamp} [react/view {:style styles/timestamp}
[chat-item/message-timestamp (:timestamp last-message)]]]))) [chat-item/message-timestamp (:timestamp last-message)]]])))

View File

@ -7,6 +7,7 @@
[status-im.chat.commands.receiving :as commands-receiving] [status-im.chat.commands.receiving :as commands-receiving]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im.ui.screens.home.styles :as styles] [status-im.ui.screens.home.styles :as styles]
[status-im.ui.screens.chat.utils :as chat.utils]
[status-im.ui.components.styles :as component.styles] [status-im.ui.components.styles :as component.styles]
[status-im.utils.core :as utils] [status-im.utils.core :as utils]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
@ -34,7 +35,10 @@
:accessibility-label :no-messages-text} :accessibility-label :no-messages-text}
(i18n/label :t/no-messages)] (i18n/label :t/no-messages)]
(str/blank? content) (= constants/content-type-command content-type)
[command-short-preview message]
(str/blank? (:text content))
[react/text {:style styles/last-message-text} [react/text {:style styles/last-message-text}
""] ""]
@ -42,11 +46,10 @@
[react/text {:style styles/last-message-text [react/text {:style styles/last-message-text
:number-of-lines 1 :number-of-lines 1
:accessibility-label :chat-message-text} :accessibility-label :chat-message-text}
#_(if-let [render-recipe (:render-recipe content)]
(chat.utils/render-chunks render-recipe message))
(:text content)] (:text content)]
(= constants/content-type-command content-type)
[command-short-preview message]
:else :else
[react/text {:style styles/last-message-text [react/text {:style styles/last-message-text
:number-of-lines 1 :number-of-lines 1

View File

@ -9,21 +9,24 @@
(:metadata (message-content/enrich-content {:text "Some *styling* present"})))) (:metadata (message-content/enrich-content {:text "Some *styling* present"}))))
(is (= {:bold [[5 14]] (is (= {:bold [[5 14]]
:tag [[28 33] [38 43]]} :tag [[28 33] [38 43]]}
(:metadata (message-content/enrich-content {:text "Some *styling* present with #tag1 and #tag2 as well"})))))) (:metadata (message-content/enrich-content {:text "Some *styling* present with #tag1 and #tag2 as well"})))))
(testing "right to left is correctly identified"
(is (not (:rtl? (message-content/enrich-content {:text "You are lucky today!"}))))
(is (not (:rtl? (message-content/enrich-content {:text "42"}))))
(is (not (:rtl? (message-content/enrich-content {:text "You are lucky today! أنت محظوظ اليوم!"}))))
(is (not (:rtl? (message-content/enrich-content {:text "۱۲۳۴۵۶۷۸۹"}))))
(is (not (:rtl? (message-content/enrich-content {:text "۱۲۳۴۵۶۷۸۹أنت محظوظ اليوم!"}))))
(is (:rtl? (message-content/enrich-content {:text "أنت محظوظ اليوم!"})))
(is (:rtl? (message-content/enrich-content {:text "أنت محظوظ اليوم! You are lucky today"})))
(is (:rtl? (message-content/enrich-content {:text "יש לך מזל היום!"})))))
(deftest build-render-recipe-test (deftest build-render-recipe-test
(testing "Render tree is build from text" (testing "Render tree is build from text"
(is (not (message-content/build-render-recipe (message-content/enrich-content {:text "Plain message"})))) (is (not (:render-recipe (message-content/enrich-content {:text "Plain message"}))))
(is (= '(["Test " #{:text}] (is (= '(["Test " :text]
["#status" #{:tag :text}] ["#status" :tag]
[" one three " #{:text}] [" one three " :text]
["#core-chat" #{:tag :bold :text}] ["#core-chat (@developer)!" :bold]
[" (" #{:bold :text}] [" By the way, " :text]
["@developer" #{:mention :bold :text}] ["nice link(https://link.com)" :italic])
[")!" #{:bold :text}] (:render-recipe (message-content/enrich-content {:text "Test #status one three *#core-chat (@developer)!* By the way, ~nice link(https://link.com)~"}))))))
[" By the way, " #{:text}]
["nice link(" #{:italic :text}]
["https://link.com" #{:link :italic :text}]
[")", #{:italic :text}])
(message-content/build-render-recipe
(message-content/enrich-content {:text "Test #status one three *#core-chat (@developer)!* By the way, ~nice link(https://link.com)~"}))))))

View File

@ -1,39 +0,0 @@
(ns status-im.test.chat.views.message
(:require [cljs.test :refer [deftest is]]
[status-im.ui.screens.chat.message.message :as message]))
(deftest parse-url
(is (= (lazy-seq [{:text "" :url? false}
{:text "www.google.com" :url? true}])
(message/parse-url "www.google.com")))
(is (= (lazy-seq [{:text "" :url? false}
{:text "status.im" :url? true}])
(message/parse-url "status.im")))
(is (= (lazy-seq [{:text "$33.90" :url? false} nil])
(message/parse-url "$33.90")))
(is (= (lazy-seq [{:text "" :url? false}
{:text "https://www.google.com/?gfe_rd=cr&dcr=0&ei=P9-CWuyBGaro8AeqkYGQDQ&gws_rd=cr&fg=1" :url? true}])
(message/parse-url "https://www.google.com/?gfe_rd=cr&dcr=0&ei=P9-CWuyBGaro8AeqkYGQDQ&gws_rd=cr&fg=1")))
(is (= (lazy-seq [{:text "Status - " :url? false}
{:text "https://github.com/status-im/status-react" :url? true}
{:text " a Mobile Ethereum Operating System" :url? false}
nil])
(message/parse-url "Status - https://github.com/status-im/status-react a Mobile Ethereum Operating System")))
(is (= (lazy-seq [{:text "Browse, chat and make payments securely on the decentralized web." :url? false} nil])
(message/parse-url "Browse, chat and make payments securely on the decentralized web.")))
(is (= (lazy-seq [{:text "test...test..." :url? false} nil])
(message/parse-url "test...test...")))
(is (= (lazy-seq [{:text "test..test.." :url? false} nil])
(message/parse-url "test..test..")))
(is (= (lazy-seq [{:text "...test" :url? false} nil])
(message/parse-url "...test"))))
(deftest right-to-left-text?
(is (not (message/right-to-left-text? "You are lucky today!")))
(is (not (message/right-to-left-text? "42")))
(is (not (message/right-to-left-text? "You are lucky today! أنت محظوظ اليوم!")))
(is (not (message/right-to-left-text? "۱۲۳۴۵۶۷۸۹")))
(is (not (message/right-to-left-text? "۱۲۳۴۵۶۷۸۹أنت محظوظ اليوم!")))
(is (message/right-to-left-text? "أنت محظوظ اليوم!"))
(is (message/right-to-left-text? "أنت محظوظ اليوم! You are lucky today"))
(is (message/right-to-left-text? "יש לך מזל היום!")))

View File

@ -27,7 +27,6 @@
[status-im.test.chat.models.message] [status-im.test.chat.models.message]
[status-im.test.chat.models.message-content] [status-im.test.chat.models.message-content]
[status-im.test.chat.subs] [status-im.test.chat.subs]
[status-im.test.chat.views.message]
[status-im.test.chat.views.photos] [status-im.test.chat.views.photos]
[status-im.test.chat.commands.core] [status-im.test.chat.commands.core]
[status-im.test.chat.commands.input] [status-im.test.chat.commands.input]
@ -93,7 +92,6 @@
'status-im.test.chat.models.input 'status-im.test.chat.models.input
'status-im.test.chat.models.message 'status-im.test.chat.models.message
'status-im.test.chat.models.message-content 'status-im.test.chat.models.message-content
'status-im.test.chat.views.message
'status-im.test.chat.views.photos 'status-im.test.chat.views.photos
'status-im.test.chat.commands.core 'status-im.test.chat.commands.core
'status-im.test.chat.commands.input 'status-im.test.chat.commands.input