Plug-in new text parsing engine
This commit is contained in:
parent
8d168bf3cd
commit
ff97345f07
|
@ -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."
|
||||||
|
|
|
@ -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?]
|
||||||
;; TODO janherich: enable the animations again once we can do them more efficiently
|
(let [emoji? (message-content/emoji-only-content? content)]
|
||||||
(cond-> (assoc message :appearing? true)
|
;; TODO janherich: enable the animations again once we can do them more efficiently
|
||||||
(not current-chat?) (assoc :appearing? false)
|
(cond-> (assoc message :appearing? true)
|
||||||
(message-content/emoji-only-content? content) (assoc :content-type constants/content-type-emoji)))
|
(not current-chat?)
|
||||||
|
(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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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}])
|
||||||
|
|
|
@ -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)))))))
|
||||||
|
|
||||||
|
|
|
@ -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]]
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -20,11 +20,11 @@
|
||||||
@unviewed-messages-count]])))
|
@unviewed-messages-count]])))
|
||||||
|
|
||||||
(views/defview chat-list-item-inner-view [{:keys [chat-id name group-chat color public? public-key] :as chat-item}]
|
(views/defview chat-list-item-inner-view [{:keys [chat-id name group-chat color public? public-key] :as chat-item}]
|
||||||
(letsubs [photo-path [:get-chat-photo chat-id]
|
(letsubs [photo-path [:get-chat-photo chat-id]
|
||||||
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)]]])))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)~"}))))))
|
|
||||||
|
|
|
@ -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? "יש לך מזל היום!")))
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue