Plug-in new text parsing engine
This commit is contained in:
parent
8d168bf3cd
commit
ff97345f07
|
@ -3,7 +3,7 @@
|
|||
[clojure.set :as set]
|
||||
[pluto.reader.hooks :as hooks]
|
||||
[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.impl.transactions :as transactions]
|
||||
[status-im.utils.handlers :as handlers]
|
||||
|
@ -22,7 +22,7 @@
|
|||
"Given the command instance, returns command name as displayed in chat input,
|
||||
with leading `/` character."
|
||||
[type]
|
||||
(str chat-constants/command-char (protocol/id type)))
|
||||
(str chat.constants/command-char (protocol/id type)))
|
||||
|
||||
(defn command-description
|
||||
"Returns description for the command."
|
||||
|
|
|
@ -36,11 +36,18 @@
|
|||
:message message})))
|
||||
|
||||
(defn- prepare-message
|
||||
[{:keys [content] :as message} chat-id current-chat?]
|
||||
;; TODO janherich: enable the animations again once we can do them more efficiently
|
||||
(cond-> (assoc message :appearing? true)
|
||||
(not current-chat?) (assoc :appearing? false)
|
||||
(message-content/emoji-only-content? content) (assoc :content-type constants/content-type-emoji)))
|
||||
[{: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
|
||||
(cond-> (assoc message :appearing? true)
|
||||
(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
|
||||
"Relative datemarks of message groups can get obsolete with passing time,
|
||||
|
@ -326,6 +333,10 @@
|
|||
(add-message-type chat))]
|
||||
(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
|
||||
|
||||
(re-frame.core/reg-fx
|
||||
|
|
|
@ -2,109 +2,103 @@
|
|||
(:require [clojure.string :as string]
|
||||
[status-im.constants :as constants]))
|
||||
|
||||
(def ^:private actions {:link constants/regx-url
|
||||
:tag constants/regx-tag
|
||||
:mention constants/regx-mention})
|
||||
(def stylings [[:bold constants/regx-bold]
|
||||
[:italic constants/regx-italic]
|
||||
[:backquote constants/regx-backquote]])
|
||||
|
||||
(def ^:private stylings {:bold constants/regx-bold
|
||||
:italic constants/regx-italic})
|
||||
(def styling-keys (into #{} (map first) stylings))
|
||||
|
||||
(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]
|
||||
(and (seq text)
|
||||
(re-matches constants/regx-rtl-characters (first text))))
|
||||
(defn- clear-ranges [ranges input]
|
||||
(reduce (fn [acc [start end]]
|
||||
(.concat (subs acc 0 start) (blank-string (- end start)) (subs acc end)))
|
||||
input ranges))
|
||||
|
||||
(defn- query-regex [regex content]
|
||||
(loop [input content
|
||||
matches []
|
||||
offset 0]
|
||||
(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)
|
||||
start-index (+ offset relative-index)
|
||||
end-index (+ start-index (count match-value))]
|
||||
(recur (apply str (drop end-index input))
|
||||
end-index (+ start-index match-size)]
|
||||
(recur (subs input (+ relative-index match-size))
|
||||
(conj matches [start-index end-index])
|
||||
end-index))
|
||||
(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
|
||||
"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
|
||||
or `:bold` and `:italic` stylings.
|
||||
Value for each key is sequence of tuples representing ranges in original
|
||||
`:text` content. "
|
||||
[{:keys [text] :as content}]
|
||||
(let [metadata (reduce-kv (fn [metadata type regex]
|
||||
(if-let [matches (query-regex regex text)]
|
||||
(assoc metadata type matches)
|
||||
metadata))
|
||||
{}
|
||||
type->regex)]
|
||||
(let [[_ metadata] (reduce (fn [[text metadata] [type regex]]
|
||||
(if-let [matches (query-regex regex text)]
|
||||
[(clear-ranges matches text) (assoc metadata type matches)]
|
||||
[text metadata]))
|
||||
[text {}]
|
||||
(into stylings actions))]
|
||||
(cond-> content
|
||||
(seq metadata) (assoc :metadata metadata)
|
||||
(right-to-left-text? text) (assoc :rtl? true))))
|
||||
|
||||
(defn- sorted-ranges [{:keys [metadata text]}]
|
||||
(->> metadata
|
||||
(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])))))))))
|
||||
(seq metadata) (as-> content
|
||||
(assoc content :metadata metadata)
|
||||
(assoc content :render-recipe (build-render-recipe content)))
|
||||
(right-to-left-text? text) (assoc :rtl? true)
|
||||
(should-collapse? text) (assoc :should-collapse? true))))
|
||||
|
||||
(defn emoji-only-content?
|
||||
"Determines if text is just an emoji"
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
(def ethereum-rpc-url "http://localhost:8545")
|
||||
|
||||
(def content-type-text "text/plain")
|
||||
(def content-type-status "status")
|
||||
(def content-type-command "command")
|
||||
(def content-type-command-request "command-request")
|
||||
(def content-type-status "status")
|
||||
(def content-type-emoji "emoji")
|
||||
|
||||
(def desktop-content-types
|
||||
|
@ -211,6 +211,10 @@
|
|||
(def regx-mention #"@[a-z0-9\-]+")
|
||||
(def regx-bold #"\*[^*]+\*")
|
||||
(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-web3 "web3")
|
||||
|
|
|
@ -197,6 +197,8 @@
|
|||
browser/v8
|
||||
dapp-permissions/v9])
|
||||
|
||||
(def v20 v19)
|
||||
|
||||
;; put schemas ordered by version
|
||||
(def schemas [{:schema v1
|
||||
:schemaVersion 1
|
||||
|
@ -254,4 +256,7 @@
|
|||
:migration migrations/v18}
|
||||
{:schema v19
|
||||
: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
|
||||
(: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]
|
||||
(log/debug "migrating v1 account database: " old-realm new-realm))
|
||||
|
@ -116,3 +117,14 @@
|
|||
|
||||
(defn v19 [old-realm new-realm]
|
||||
(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]]
|
||||
(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
|
||||
:chat.ui/show-profile
|
||||
(fn [cofx [_ identity]]
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
[status-im.constants :as constants]
|
||||
[status-im.ui.components.chat-icon.screen :as chat-icon.screen]
|
||||
[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.gfycat.core :as gfycat]
|
||||
[status-im.utils.platform :as platform]
|
||||
|
@ -29,17 +29,9 @@
|
|||
(commands/generate-preview command command-message)
|
||||
[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]
|
||||
(when-not command?
|
||||
(let [rtl? (right-to-left-text? (:text content))]
|
||||
[react/text {:style (style/message-timestamp-text justify-timestamp? outgoing rtl?)} t])))
|
||||
[react/text {:style (style/message-timestamp-text justify-timestamp? outgoing (:rtl? content))} t]))
|
||||
|
||||
(defn message-view
|
||||
[{:keys [timestamp-str outgoing content] :as message} message-content {:keys [justify-timestamp?]}]
|
||||
|
@ -49,111 +41,19 @@
|
|||
(get content :command-ref))
|
||||
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
|
||||
; so we pad the invisible placeholder with some spaces to avoid having too
|
||||
; close to the text.
|
||||
(defn timestamp-with-padding [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]
|
||||
(letsubs [username [:get-contact-name-by-identity from]]
|
||||
[react/view {:style (style/quoted-message-container outgoing)}
|
||||
[react/view {:style style/quoted-message-author-container}
|
||||
[vector-icons/icon :icons/reply {:color (if outgoing colors/wild-blue-yonder colors/gray)}]
|
||||
[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)
|
||||
:number-of-lines 5}
|
||||
text]]))
|
||||
|
@ -162,32 +62,30 @@
|
|||
[react/view style/status-container
|
||||
[react/text {:style style/status-text
|
||||
: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
|
||||
[{: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
|
||||
(let [parsed-text (cached-parse-text (:text content) :browser.ui/message-link-pressed outgoing)
|
||||
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?))))]
|
||||
(let [collapsible? (and (:should-collapse? content) group-chat)]
|
||||
[react/view
|
||||
(when (:response-to content)
|
||||
[quoted-message (:response-to content) outgoing current-public-key])
|
||||
[react/text {:style (style/text-message collapsible? outgoing)
|
||||
:number-of-lines (when collapsible? number-of-lines)
|
||||
:ref (partial reset! ref)}
|
||||
parsed-text
|
||||
[react/text {:style (style/message-timestamp-placeholder-text outgoing)} (timestamp-with-padding timestamp-str)]]
|
||||
[react/text (cond-> {:style (style/text-message collapsible? outgoing)}
|
||||
(and collapsible? (not expanded?))
|
||||
(assoc :number-of-lines constants/lines-collapse-threshold))
|
||||
(if-let [render-recipe (:render-recipe content)]
|
||||
(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?
|
||||
[expand-button collapsed? on-press])])
|
||||
[expand-button expanded? chat-id message-id])])
|
||||
{:justify-timestamp? true}])
|
||||
|
||||
(defn emoji-message
|
||||
|
@ -314,7 +212,7 @@
|
|||
(defview message-author-name [from message-username]
|
||||
(letsubs [username [:get-contact-name-by-identity from]]
|
||||
[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
|
||||
[{:keys [last-in-group?
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
{:color colors/gray
|
||||
:font-size 12
|
||||
:opacity 0.7
|
||||
:margin-bottom 1})
|
||||
:margin-bottom 20})
|
||||
|
||||
(def selected-message
|
||||
{:margin-top 18
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
(ns status-im.ui.screens.chat.utils
|
||||
(:require [status-im.utils.gfycat.core :as gfycat]
|
||||
[status-im.i18n :as i18n]))
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[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]
|
||||
(str (when username (str username " :: "))
|
||||
|
@ -10,3 +15,41 @@
|
|||
(or (and (= from current-public-key) (i18n/label :t/You))
|
||||
(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.i18n :as i18n]
|
||||
[status-im.ui.screens.desktop.main.chat.events :as chat.events]
|
||||
[status-im.ui.screens.chat.message.message :as chat.message]
|
||||
[status-im.utils.http :as http]))
|
||||
[status-im.ui.screens.chat.message.message :as chat.message]))
|
||||
|
||||
(views/defview toolbar-chat-view [{:keys [chat-id color public-key public? group-chat]
|
||||
:as current-chat}]
|
||||
|
@ -109,31 +108,11 @@
|
|||
:number-of-lines 5}
|
||||
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]
|
||||
(not= (get-in user-statuses [current-public-key :status]) :not-sent))
|
||||
|
||||
(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/touchable-highlight {:on-press #(if (= "right" (.-button (.-nativeEvent %)))
|
||||
(do (utils/show-popup "" "Message copied to clipboard")
|
||||
|
@ -143,10 +122,12 @@
|
|||
[react/view {:style styles/message-container}
|
||||
(when (:response-to content)
|
||||
[quoted-message (:response-to content) false current-public-key])
|
||||
(into [react/text {:style (styles/message-text false)
|
||||
:selectable true
|
||||
:selection-color colors/blue-light}]
|
||||
(process-message-links text))]]])
|
||||
[react/text {:style (styles/message-text false)
|
||||
:selectable true
|
||||
:selection-color colors/blue-light}
|
||||
(if-let [render-recipe (:render-recipe content)]
|
||||
(chat-utils/render-chunks render-recipe message)
|
||||
(:text content))]]]])
|
||||
|
||||
(views/defview photo-placeholder []
|
||||
[react/view {:style {:width 40
|
||||
|
|
|
@ -20,11 +20,11 @@
|
|||
@unviewed-messages-count]])))
|
||||
|
||||
(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]
|
||||
unviewed-messages-count [:unviewed-messages-count chat-id]
|
||||
chat-name [:get-chat-name chat-id]
|
||||
current-chat-id [:get-current-chat-id]
|
||||
last-message [:get-last-message chat-id]]
|
||||
(letsubs [photo-path [:get-chat-photo chat-id]
|
||||
unviewed-messages-count [:unviewed-messages-count chat-id]
|
||||
chat-name [:get-chat-name chat-id]
|
||||
current-chat-id [:get-current-chat-id]
|
||||
{:keys [content] :as last-message} [:get-last-message chat-id]]
|
||||
(let [name (or chat-name
|
||||
(gfycat/generate-gfy public-key))
|
||||
[unviewed-messages-label large?] (if (< 9 unviewed-messages-count)
|
||||
|
@ -57,7 +57,8 @@
|
|||
:style styles/chat-last-message}
|
||||
(if (= constants/content-type-command (:content-type 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}
|
||||
[chat-item/message-timestamp (:timestamp last-message)]]])))
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
[status-im.chat.commands.receiving :as commands-receiving]
|
||||
[status-im.ui.components.react :as react]
|
||||
[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.utils.core :as utils]
|
||||
[status-im.i18n :as i18n]
|
||||
|
@ -34,7 +35,10 @@
|
|||
:accessibility-label :no-messages-text}
|
||||
(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}
|
||||
""]
|
||||
|
||||
|
@ -42,11 +46,10 @@
|
|||
[react/text {:style styles/last-message-text
|
||||
:number-of-lines 1
|
||||
:accessibility-label :chat-message-text}
|
||||
#_(if-let [render-recipe (:render-recipe content)]
|
||||
(chat.utils/render-chunks render-recipe message))
|
||||
(:text content)]
|
||||
|
||||
(= constants/content-type-command content-type)
|
||||
[command-short-preview message]
|
||||
|
||||
:else
|
||||
[react/text {:style styles/last-message-text
|
||||
:number-of-lines 1
|
||||
|
|
|
@ -9,21 +9,24 @@
|
|||
(:metadata (message-content/enrich-content {:text "Some *styling* present"}))))
|
||||
(is (= {:bold [[5 14]]
|
||||
: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
|
||||
(testing "Render tree is build from text"
|
||||
(is (not (message-content/build-render-recipe (message-content/enrich-content {:text "Plain message"}))))
|
||||
(is (= '(["Test " #{:text}]
|
||||
["#status" #{:tag :text}]
|
||||
[" one three " #{:text}]
|
||||
["#core-chat" #{:tag :bold :text}]
|
||||
[" (" #{:bold :text}]
|
||||
["@developer" #{:mention :bold :text}]
|
||||
[")!" #{:bold :text}]
|
||||
[" 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)~"}))))))
|
||||
(is (not (:render-recipe (message-content/enrich-content {:text "Plain message"}))))
|
||||
(is (= '(["Test " :text]
|
||||
["#status" :tag]
|
||||
[" one three " :text]
|
||||
["#core-chat (@developer)!" :bold]
|
||||
[" By the way, " :text]
|
||||
["nice link(https://link.com)" :italic])
|
||||
(: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-content]
|
||||
[status-im.test.chat.subs]
|
||||
[status-im.test.chat.views.message]
|
||||
[status-im.test.chat.views.photos]
|
||||
[status-im.test.chat.commands.core]
|
||||
[status-im.test.chat.commands.input]
|
||||
|
@ -93,7 +92,6 @@
|
|||
'status-im.test.chat.models.input
|
||||
'status-im.test.chat.models.message
|
||||
'status-im.test.chat.models.message-content
|
||||
'status-im.test.chat.views.message
|
||||
'status-im.test.chat.views.photos
|
||||
'status-im.test.chat.commands.core
|
||||
'status-im.test.chat.commands.input
|
||||
|
|
Loading…
Reference in New Issue