Render markdown
Fixes: https://github.com/status-im/trailofbits-audit/issues/47 Fixes: https://github.com/status-im/trailofbits-audit/issues/46 Fixes: https://github.com/status-im/trailofbits-audit/issues/44 Fixes: https://github.com/status-im/security-reports/issues/13 Fixes: https://github.com/status-im/security-reports/issues/5 Fixes: https://github.com/status-im/status-react/issues/8995 This commits re-introduce rendering of markdown text and implent a few changes: 1) Parsing of the message content is now in status-go, this includes markdown, line-count, and rtl. Parsing is not nested, as there's some rendering degradation involved as we nest components, unclear exactly if it's react-native or clojure, haven't looked too deeply into it. 2) Emojii type messages are not parsed on the sending side, not the receiving one, using the appropriate content-type 3) Fixes a few issues with chat input rendering, currrently we use `chats/current-chat` subscription which is very heavy and should not be used unless necessary, and means that any change to chat will trigger a re-render, which caused re-rendering of input container on each received message. Also to note that input-container is fairly heavy to render, and it's rendered twice at each keypress on input. The inline markdow supported is: *italic* or _italic_ **bold** or __bold__ `inline code` http://test.com links \#status-tag The block markdown supported is: \# Headers ``` code blocks ``` > Quotereply The styling is very basic at the moment, but can be improved. Adding other markdown (photo,mentions) is straightforward and should come at little performance cost (unless the component to render is heavy, i.e a photo for example). There are some behavioral changes with this commit: 1) Links are only parsed if starting with http:// or https://, meaning that blah.com won't be parsed, nor www.test.com. This behavior is consistent with discord for example and allows faster parsing at little expense to ser experience imo. Fixes a few security issues as well. 2) Content is not anymore capped (regression), that's due to the fact that before we only rendered text and react-native allowed us easily to limit the number of lines, but adding markdown support means that this strategy is not viable anymore. Performance of rendering don't see to be very much impacted by this, I would re-introduce it if necessary, but I'd rather do that in a separate PR. Signed-off-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
This commit is contained in:
parent
3127f2fcb2
commit
9a9c0ce526
|
@ -589,6 +589,8 @@ var TopLevel = {
|
|||
"prev": function() {},
|
||||
"hasNext": function() {},
|
||||
"hasPrev": function() {},
|
||||
"rtl": function() {},
|
||||
"lineCount": function() {},
|
||||
"key": function() {},
|
||||
"keys": function() {},
|
||||
"values": function() {},
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
[status-im.chat.commands.sending :as commands.sending]
|
||||
[status-im.chat.constants :as chat.constants]
|
||||
[status-im.chat.models :as chat]
|
||||
[status-im.chat.models.message-content :as message-content]
|
||||
[status-im.chat.models.message :as chat.message]
|
||||
[status-im.constants :as constants]
|
||||
[status-im.js-dependencies :as dependencies]
|
||||
|
@ -125,11 +126,15 @@
|
|||
(let [{:keys [message-id]}
|
||||
(get-in db [:chats current-chat-id :metadata :responding-to-message])
|
||||
show-name? (get-in db [:multiaccount :show-name?])
|
||||
preferred-name (when show-name? (get-in db [:multiaccount :preferred-name]))]
|
||||
preferred-name (when show-name? (get-in db [:multiaccount :preferred-name]))
|
||||
emoji? (message-content/emoji-only-content? {:text input-text
|
||||
:response-to message-id})]
|
||||
(fx/merge cofx
|
||||
{:db (assoc-in db [:chats current-chat-id :metadata :responding-to-message] nil)}
|
||||
(chat.message/send-message {:chat-id current-chat-id
|
||||
:content-type constants/content-type-text
|
||||
:content-type (if emoji?
|
||||
constants/content-type-emoji
|
||||
constants/content-type-text)
|
||||
:content (cond-> {:chat-id current-chat-id
|
||||
:text input-text}
|
||||
message-id
|
||||
|
@ -140,15 +145,6 @@
|
|||
(set-chat-input-text nil)
|
||||
(process-cooldown)))))
|
||||
|
||||
(defn send-plain-text-message-fx
|
||||
"no command detected, when not empty, proceed by sending text message without command processing"
|
||||
[{:keys [db] :as cofx} message-text current-chat-id]
|
||||
(when-not (string/blank? message-text)
|
||||
(chat.message/send-message cofx {:chat-id current-chat-id
|
||||
:content-type constants/content-type-text
|
||||
:content (cond-> {:chat-id current-chat-id
|
||||
:text message-text})})))
|
||||
|
||||
(fx/defn send-sticker-fx
|
||||
[{:keys [db] :as cofx} {:keys [hash pack]} current-chat-id]
|
||||
(when-not (string/blank? hash)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
(:require [re-frame.core :as re-frame]
|
||||
[status-im.multiaccounts.model :as multiaccounts.model]
|
||||
[status-im.chat.commands.receiving :as commands-receiving]
|
||||
[status-im.ethereum.json-rpc :as json-rpc]
|
||||
[status-im.chat.db :as chat.db]
|
||||
[status-im.chat.models :as chat-model]
|
||||
[status-im.chat.models.loading :as chat-loading]
|
||||
|
@ -37,17 +38,14 @@
|
|||
|
||||
(defn- prepare-message
|
||||
[{: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-> message
|
||||
current-chat?
|
||||
(assoc :seen true)
|
||||
(assoc :seen true) (and (= constants/content-type-text content-type)
|
||||
(message-content/should-collapse?
|
||||
(:text content)
|
||||
(:line-count content)))
|
||||
|
||||
emoji?
|
||||
(assoc :content-type constants/content-type-emoji)
|
||||
|
||||
(and (= constants/content-type-text content-type) (not emoji?))
|
||||
(update :content message-content/enrich-content))))
|
||||
(assoc :should-collapse? true)))
|
||||
|
||||
(defn system-message? [{:keys [message-type]}]
|
||||
(= :system-message message-type))
|
||||
|
@ -250,24 +248,46 @@
|
|||
:message message
|
||||
:current-chat? (= (get-in cofx [:db :current-chat-id]) chat-id)})))
|
||||
|
||||
(fx/defn upsert-and-send
|
||||
[{:keys [now] :as cofx} {:keys [chat-id from] :as message}]
|
||||
(let [message (remove-icon message)
|
||||
send-record (protocol/map->Message (select-keys message transport-keys))
|
||||
(fx/defn prepare-message-content [cofx chat-id message]
|
||||
{::json-rpc/call
|
||||
[{:method "shhext_prepareContent"
|
||||
:params [(:content message)]
|
||||
:on-success #(re-frame/dispatch [::prepared-message chat-id message %])
|
||||
:on-failure #(log/error "failed to prepare content" %)}]})
|
||||
|
||||
(fx/defn prepared-message
|
||||
{:events [::prepared-message]}
|
||||
[{:keys [now] :as cofx} chat-id message content]
|
||||
(let [message-with-content
|
||||
(update message :content
|
||||
assoc
|
||||
:parsed-text (:parsedText content)
|
||||
:line-count (:lineCount content)
|
||||
:should-collapse? (message-content/should-collapse?
|
||||
(:text content)
|
||||
(:lineCount content))
|
||||
:rtl? (:rtl content))
|
||||
send-record (protocol/map->Message
|
||||
(select-keys message-with-content transport-keys))
|
||||
wrapped-record (if (= (:message-type send-record) :group-user-message)
|
||||
(wrap-group-message cofx chat-id send-record)
|
||||
send-record)
|
||||
message (assoc message :outgoing-status :sending)]
|
||||
|
||||
send-record)]
|
||||
(fx/merge cofx
|
||||
(chat-model/upsert-chat
|
||||
{:chat-id chat-id
|
||||
:timestamp now
|
||||
:last-message-timestamp (:timestamp message)
|
||||
:last-message-content (:content message)
|
||||
:last-message-content-type (:content-type message)
|
||||
:last-clock-value (:clock-value message)})
|
||||
(send chat-id message wrapped-record))))
|
||||
:last-message-timestamp (:timestamp message-with-content)
|
||||
:last-message-content (:content message-with-content)
|
||||
:last-message-content-type (:content-type message-with-content)
|
||||
:last-clock-value (:clock-value message-with-content)})
|
||||
(send chat-id message-with-content wrapped-record))))
|
||||
|
||||
(fx/defn upsert-and-send
|
||||
[{:keys [now] :as cofx} {:keys [chat-id from] :as message}]
|
||||
(let [message (remove-icon message)
|
||||
message (assoc message :outgoing-status :sending)]
|
||||
|
||||
(prepare-message-content cofx chat-id message)))
|
||||
|
||||
(fx/defn update-message-status
|
||||
[{:keys [db] :as cofx} chat-id message-id status]
|
||||
|
|
|
@ -40,9 +40,9 @@
|
|||
(and (seq text)
|
||||
(re-matches constants/regx-rtl-characters (first text))))
|
||||
|
||||
(defn- should-collapse? [text]
|
||||
(defn should-collapse? [text line-count]
|
||||
(or (<= constants/chars-collapse-threshold (count text))
|
||||
(<= constants/lines-collapse-threshold (inc (count (query-regex #"\n" text))))))
|
||||
(<= constants/lines-collapse-threshold (inc line-count))))
|
||||
|
||||
(defn- sorted-ranges [{:keys [metadata text]} metadata-keys]
|
||||
(->> (if metadata-keys
|
||||
|
@ -81,28 +81,6 @@
|
|||
(cond-> builder
|
||||
end-record (conj end-record))))))
|
||||
|
||||
(defn enrich-content
|
||||
"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 (fn [[text metadata] [type regex]]
|
||||
(if-let [matches (query-regex regex text)]
|
||||
[(clear-ranges matches text) (assoc metadata type matches)]
|
||||
[text metadata]))
|
||||
[text {}]
|
||||
(if platform/desktop?
|
||||
(into stylings actions)
|
||||
actions))]
|
||||
(cond-> content
|
||||
(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"
|
||||
[{:keys [text response-to]}]
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"shhext_chatMessages" {}
|
||||
"shhext_saveChat" {}
|
||||
"shhext_contacts" {}
|
||||
"shhext_prepareContent" {}
|
||||
"shhext_blockContact" {}
|
||||
;;TODO not used anywhere?
|
||||
"shhext_deleteChat" {}
|
||||
|
|
|
@ -616,11 +616,6 @@
|
|||
{:db (assoc db :view-id view-id)}
|
||||
#(mark-messages-seen %))))
|
||||
|
||||
(handlers/register-handler-fx
|
||||
:chat/send-plain-text-message
|
||||
(fn [{{:keys [current-chat-id]} :db :as cofx} [_ message-text]]
|
||||
(chat.input/send-plain-text-message-fx cofx message-text current-chat-id)))
|
||||
|
||||
(handlers/register-handler-fx
|
||||
:chat/send-sticker
|
||||
(fn [{{:keys [current-chat-id multiaccount]} :db :as cofx} [_ {:keys [hash] :as sticker}]]
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
(:require [clojure.set :as clojure.set]
|
||||
[clojure.spec.alpha :as spec]
|
||||
[clojure.string :as string]
|
||||
[status-im.ethereum.json-rpc :as json-rpc]
|
||||
[re-frame.core :as re-frame]
|
||||
[status-im.chat.models.message-content :as message-content]
|
||||
[status-im.multiaccounts.core :as multiaccounts]
|
||||
[status-im.multiaccounts.model :as multiaccounts.model]
|
||||
[status-im.utils.pairing :as pairing.utils]
|
||||
|
@ -463,6 +465,43 @@
|
|||
(transport.filters/upsert-group-chat-topics)
|
||||
(transport.filters/load-members members)))))
|
||||
|
||||
(fx/defn prepared-message
|
||||
{:events [::prepared-message]}
|
||||
[{:keys [now] :as cofx}
|
||||
chat-id message
|
||||
content
|
||||
sender-signature
|
||||
whisper-timestamp
|
||||
metadata]
|
||||
(let [message-with-content
|
||||
(update message :content
|
||||
assoc
|
||||
:parsed-text (:parsedText content)
|
||||
:line-count (:lineCount content)
|
||||
:should-collapse? (message-content/should-collapse?
|
||||
(:text content)
|
||||
(:lineCount content))
|
||||
:rtl? (:rtl content))]
|
||||
(protocol/receive message-with-content
|
||||
chat-id
|
||||
sender-signature
|
||||
whisper-timestamp
|
||||
(assoc cofx :metadata metadata))))
|
||||
|
||||
(fx/defn prepare-message-content
|
||||
[cofx chat-id message sender-signature whisper-timestamp metadata]
|
||||
{::json-rpc/call
|
||||
[{:method "shhext_prepareContent"
|
||||
:params [(:content message)]
|
||||
:on-success #(re-frame/dispatch [::prepared-message
|
||||
chat-id
|
||||
message
|
||||
%
|
||||
sender-signature
|
||||
whisper-timestamp
|
||||
metadata])
|
||||
:on-failure #(log/error "failed to prepare content" %)}]})
|
||||
|
||||
(fx/defn handle-membership-update
|
||||
"Upsert chat and receive message if valid"
|
||||
;; Care needs to be taken here as chat-id is not coming from a whisper filter
|
||||
|
@ -498,11 +537,13 @@
|
|||
;; don't allow anything but group messages
|
||||
(instance? protocol/Message message)
|
||||
(= :group-user-message (:message-type message)))
|
||||
(protocol/receive message
|
||||
(prepare-message-content
|
||||
%
|
||||
chat-id
|
||||
message
|
||||
sender-signature
|
||||
whisper-timestamp
|
||||
(assoc % :metadata metadata))))))))
|
||||
metadata)))))))
|
||||
|
||||
(defn handle-sign-success
|
||||
"Upsert chat and send signed payload to group members"
|
||||
|
|
|
@ -505,9 +505,9 @@
|
|||
(re-frame/reg-sub
|
||||
::show-suggestions-view?
|
||||
:<- [:chats/current-chat-ui-prop :show-suggestions?]
|
||||
:<- [:chats/current-chat]
|
||||
:<- [:chats/current-chat-input-text]
|
||||
:<- [:chats/all-available-commands]
|
||||
(fn [[show-suggestions? {:keys [input-text]} commands]]
|
||||
(fn [[show-suggestions? input-text commands]]
|
||||
(and (or show-suggestions?
|
||||
(commands.input/starts-as-command? (string/trim (or input-text ""))))
|
||||
(seq commands))))
|
||||
|
@ -523,7 +523,7 @@
|
|||
::get-commands-for-chat
|
||||
:<- [:chats/id->command]
|
||||
:<- [::access-scope->command-id]
|
||||
:<- [:chats/current-chat]
|
||||
:<- [:chats/current-raw-chat]
|
||||
(fn [[id->command access-scope->command-id chat]]
|
||||
(commands/chat-commands id->command access-scope->command-id chat)))
|
||||
|
||||
|
@ -666,9 +666,21 @@
|
|||
:messages))))
|
||||
|
||||
(re-frame/reg-sub
|
||||
:chats/current-chat
|
||||
:chats/current-raw-chat
|
||||
:<- [:chats/active-chats]
|
||||
:<- [:chats/current-chat-id]
|
||||
(fn [[chats current-chat-id]]
|
||||
(get chats current-chat-id)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
:chats/current-chat-input-text
|
||||
:<- [:chats/current-raw-chat]
|
||||
(fn [chat]
|
||||
(:input-text chat)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
:chats/current-chat
|
||||
:<- [:chats/current-raw-chat]
|
||||
:<- [:multiaccount/public-key]
|
||||
:<- [:mailserver/ranges]
|
||||
:<- [:chats/content-layout-height]
|
||||
|
@ -677,16 +689,17 @@
|
|||
:<- [:ethereum/chain-keyword]
|
||||
:<- [:prices]
|
||||
:<- [:wallet/currency]
|
||||
(fn [[chats current-chat-id my-public-key ranges height
|
||||
(fn [[{:keys [group-chat
|
||||
chat-id
|
||||
contact
|
||||
messages]
|
||||
:as current-chat} my-public-key ranges height
|
||||
input-height ttt-settings chain-keyword prices currency]]
|
||||
(let [{:keys [group-chat contact messages]
|
||||
:as current-chat}
|
||||
(get chats current-chat-id)]
|
||||
(when current-chat
|
||||
(cond-> (enrich-current-chat current-chat ranges height input-height)
|
||||
(empty? messages)
|
||||
(assoc :universal-link
|
||||
(links/generate-link :public-chat :external current-chat-id))
|
||||
(links/generate-link :public-chat :external chat-id))
|
||||
|
||||
(chat.models/public-chat? current-chat)
|
||||
(assoc :show-input? true)
|
||||
|
@ -697,7 +710,7 @@
|
|||
|
||||
(not group-chat)
|
||||
(enrich-current-one-to-one-chat my-public-key ttt-settings
|
||||
chain-keyword prices currency))))))
|
||||
chain-keyword prices currency)))))
|
||||
|
||||
(re-frame/reg-sub
|
||||
:chats/current-chat-message
|
||||
|
@ -798,10 +811,10 @@
|
|||
|
||||
(re-frame/reg-sub
|
||||
:chats/selected-chat-command
|
||||
:<- [:chats/current-chat]
|
||||
:<- [:chats/current-chat-input-text]
|
||||
:<- [:chats/current-chat-ui-prop :selection]
|
||||
:<- [::get-commands-for-chat]
|
||||
(fn [[{:keys [input-text]} selection commands]]
|
||||
(fn [[input-text selection commands]]
|
||||
(commands.input/selected-chat-command input-text selection commands)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
(spec/def :message.content/params (spec/map-of keyword? any?))
|
||||
|
||||
(spec/def ::content-type #{constants/content-type-text constants/content-type-command
|
||||
constants/content-type-emoji
|
||||
constants/content-type-command-request constants/content-type-sticker})
|
||||
(spec/def ::message-type #{:group-user-message :public-group-user-message :user-message})
|
||||
(spec/def ::clock-value (spec/and pos-int?
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
[status-im.transport.message.transit :as transit]
|
||||
[status-im.transport.utils :as transport.utils]
|
||||
[status-im.tribute-to-talk.whitelist :as whitelist]
|
||||
[cljs-bean.core :as clj-bean]
|
||||
[status-im.utils.config :as config]
|
||||
[status-im.utils.fx :as fx]
|
||||
[taoensso.timbre :as log]
|
||||
|
@ -19,14 +20,20 @@
|
|||
|
||||
(def message-type-message 1)
|
||||
|
||||
(defn build-content [content-js]
|
||||
{:text (.-text content-js)
|
||||
:line-count (.-lineCount content-js)
|
||||
:parsed-text (clj-bean/->clj (.-parsedText content-js))
|
||||
:name (.-name content-js)
|
||||
:rtl? (.-rtl content-js)
|
||||
:response-to (aget content-js "response-to")
|
||||
:chat-id (.-chat_id content-js)})
|
||||
|
||||
(defn build-message [parsed-message-js]
|
||||
(let [content (.-content parsed-message-js)
|
||||
built-message
|
||||
(protocol/Message.
|
||||
{:text (.-text content)
|
||||
:response-to (aget content "response-to")
|
||||
:name (.-name content)
|
||||
:chat-id (.-chat_id content)}
|
||||
(build-content content)
|
||||
(.-content_type parsed-message-js)
|
||||
(keyword (.-message_type parsed-message-js))
|
||||
(.-clock parsed-message-js)
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
(def black-transparent (alpha black 0.1)) ;; Used as background color for rounded button on dark background and as background color for containers like "Backup recovery phrase"
|
||||
(def black-transparent-20 (alpha black 0.2)) ; accounts divider
|
||||
(def black-transparent-40 (alpha black 0.4))
|
||||
|
||||
(def black-transparent-50 (alpha black 0.5))
|
||||
(def black-light "#2d2d2d") ;; sign-with-keycard-button
|
||||
|
||||
;; DARK GREY
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
[status-im.ui.screens.chat.stickers.views :as stickers]))
|
||||
|
||||
(defview basic-text-input [{:keys [set-container-width-fn height single-line-input?]}]
|
||||
(letsubs [{:keys [input-text]} [:chats/current-chat]
|
||||
(letsubs [input-text [:chats/current-chat-input-text]
|
||||
cooldown-enabled? [:chats/cooldown-enabled?]]
|
||||
[react/text-input
|
||||
(merge
|
||||
|
@ -91,7 +91,7 @@
|
|||
{:placeholder (i18n/label :cooldown/text-input-disabled)}))]))
|
||||
|
||||
(defview invisible-input [{:keys [set-layout-width-fn value]}]
|
||||
(letsubs [{:keys [input-text]} [:chats/current-chat]]
|
||||
(letsubs [input-text [:chats/current-chat-input-text]]
|
||||
[react/text {:style style/invisible-input-text
|
||||
:on-layout #(let [w (-> (.-nativeEvent %)
|
||||
(.-layout)
|
||||
|
@ -184,7 +184,7 @@
|
|||
(defview input-container []
|
||||
(letsubs [margin [:chats/input-margin]
|
||||
mainnet? [:mainnet?]
|
||||
{:keys [input-text]} [:chats/current-chat]
|
||||
input-text [:chats/current-chat-input-text]
|
||||
result-box [:chats/current-chat-ui-prop :result-box]
|
||||
show-stickers? [:chats/current-chat-ui-prop :show-stickers?]
|
||||
state-text (reagent/atom "")]
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
(:require [re-frame.core :as re-frame]
|
||||
[status-im.chat.commands.receiving :as commands-receiving]
|
||||
[status-im.constants :as constants]
|
||||
[status-im.utils.http :as http]
|
||||
[status-im.i18n :as i18n]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.ui.components.colors :as colors]
|
||||
[status-im.utils.security :as security]
|
||||
[status-im.ui.components.icons.vector-icons :as vector-icons]
|
||||
[status-im.ui.components.list-selection :as list-selection]
|
||||
[status-im.ui.components.popup-menu.views :as desktop.pop-up]
|
||||
|
@ -66,32 +69,89 @@
|
|||
:on-press #(re-frame/dispatch [:chat.ui/message-expand-toggled chat-id message-id])}
|
||||
(i18n/label (if expanded? :show-less :show-more))])
|
||||
|
||||
(defn render-inline [message-text outgoing acc {:keys [type literal destination] :as node}]
|
||||
(case type
|
||||
""
|
||||
(conj acc literal)
|
||||
|
||||
"code"
|
||||
(conj acc [react/text-class style/inline-code-style literal])
|
||||
|
||||
"emph"
|
||||
(conj acc [react/text-class (style/emph-style outgoing) literal])
|
||||
|
||||
"strong"
|
||||
(conj acc [react/text-class (style/strong-style outgoing) literal])
|
||||
|
||||
"link"
|
||||
(conj acc
|
||||
[react/text-class
|
||||
{:style
|
||||
{:color (if outgoing colors/white colors/blue)
|
||||
:text-decoration-line :underline}
|
||||
:on-press
|
||||
#(when (and (security/safe-link? destination)
|
||||
(security/safe-link-text? message-text))
|
||||
(if platform/desktop?
|
||||
(.openURL react/linking (http/normalize-url destination))
|
||||
(re-frame/dispatch
|
||||
[:browser.ui/message-link-pressed destination])))}
|
||||
destination])
|
||||
|
||||
"status-tag"
|
||||
(conj acc [react/text-class
|
||||
{:style {:color (if outgoing colors/white colors/blue)
|
||||
:text-decoration-line :underline}
|
||||
:on-press
|
||||
#(re-frame/dispatch
|
||||
[:chat.ui/start-public-chat literal {:navigation-reset? true}])}
|
||||
"#"
|
||||
literal])
|
||||
|
||||
(conj acc literal)))
|
||||
|
||||
(defn render-block [{:keys [chat-id message-id content
|
||||
timestamp-str group-chat outgoing
|
||||
current-public-key expanded?] :as message}
|
||||
acc
|
||||
{:keys [type literal children]}]
|
||||
(case type
|
||||
|
||||
"paragraph"
|
||||
(conj acc (reduce
|
||||
(fn [acc e] (render-inline (:text content) outgoing acc e))
|
||||
[react/text-class (style/text-style outgoing)]
|
||||
children))
|
||||
|
||||
"blockquote"
|
||||
(conj acc [react/view (style/blockquote-style outgoing)
|
||||
[react/text-class (style/blockquote-text-style outgoing)
|
||||
(.substring literal 0 (dec (.-length literal)))]])
|
||||
|
||||
"codeblock"
|
||||
(conj acc [react/view style/codeblock-style
|
||||
[react/text-class style/codeblock-text-style
|
||||
(.substring literal 0 (dec (.-length literal)))]])
|
||||
|
||||
acc))
|
||||
|
||||
(defn render-parsed-text [{:keys [timestamp-str
|
||||
outgoing] :as message}
|
||||
|
||||
tree]
|
||||
(conj (reduce (fn [acc e] (render-block message acc e)) [react/view {}] tree)
|
||||
[react/text {:style (style/message-timestamp-placeholder outgoing)}
|
||||
(str " " timestamp-str)]))
|
||||
|
||||
(defn text-message
|
||||
[{:keys [chat-id message-id content
|
||||
timestamp-str group-chat outgoing current-public-key expanded?] :as message}]
|
||||
[message-view message
|
||||
(let [response-to (:response-to content)
|
||||
collapsible? (and (:should-collapse? content) group-chat)]
|
||||
(let [response-to (:response-to content)]
|
||||
[react/view
|
||||
(when response-to
|
||||
(when (seq response-to)
|
||||
[quoted-message response-to (:quoted-message message) outgoing current-public-key])
|
||||
(apply react/nested-text
|
||||
(cond-> {:style (style/text-message collapsible? outgoing)
|
||||
:text-break-strategy :balanced
|
||||
:parseBasicMarkdown true
|
||||
:markdownCodeBackgroundColor colors/black
|
||||
:markdownCodeForegroundColor colors/green}
|
||||
|
||||
(and collapsible? (not expanded?))
|
||||
(assoc :number-of-lines constants/lines-collapse-threshold))
|
||||
(conj (if-let [render-recipe (:render-recipe content)]
|
||||
(chat.utils/render-chunks render-recipe message)
|
||||
[(:text content)])
|
||||
[{:style (style/message-timestamp-placeholder outgoing)}
|
||||
(str " " timestamp-str)]))
|
||||
|
||||
(when collapsible?
|
||||
[expand-button expanded? chat-id message-id])])
|
||||
[render-parsed-text message (:parsed-text content)]])
|
||||
{:justify-timestamp? true}])
|
||||
|
||||
(defn emoji-message
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
(ns status-im.ui.screens.chat.styles.message.message
|
||||
(:require [status-im.constants :as constants]
|
||||
[status-im.ui.components.colors :as colors]
|
||||
[status-im.ui.components.react :as react]
|
||||
[status-im.ui.screens.chat.styles.photos :as photos]
|
||||
[status-im.utils.platform :as platform]
|
||||
[status-im.ui.components.typography :as typography]
|
||||
[status-im.utils.styles :as styles]))
|
||||
|
||||
(defn style-message-text
|
||||
|
@ -216,3 +218,98 @@
|
|||
:color (if outgoing
|
||||
colors/white-transparent-70
|
||||
colors/gray)})
|
||||
|
||||
;; Markdown styles
|
||||
|
||||
(def default-text-style
|
||||
{:max-font-size-multiplier react/max-font-size-multiplier
|
||||
:style (assoc typography/default-style
|
||||
:line-height 22)})
|
||||
|
||||
(def outgoing-text-style
|
||||
(update default-text-style :style
|
||||
assoc :color colors/white))
|
||||
|
||||
(defn text-style [outgoing]
|
||||
(if outgoing
|
||||
outgoing-text-style
|
||||
default-text-style))
|
||||
|
||||
(def emph-text-style
|
||||
(update default-text-style :style
|
||||
assoc :font-style :italic))
|
||||
|
||||
(def outgoing-emph-text-style
|
||||
(update emph-text-style :style
|
||||
assoc :color colors/white))
|
||||
|
||||
(defn emph-style [outgoing]
|
||||
(if outgoing
|
||||
outgoing-emph-text-style
|
||||
emph-text-style))
|
||||
|
||||
(def strong-text-style
|
||||
(update default-text-style :style
|
||||
assoc :font-weight "700"))
|
||||
|
||||
(def outgoing-strong-text-style
|
||||
(update strong-text-style :style
|
||||
assoc :color colors/white))
|
||||
|
||||
(defn strong-style [outgoing]
|
||||
(if outgoing
|
||||
outgoing-strong-text-style
|
||||
strong-text-style))
|
||||
|
||||
(def monospace-fonts (if platform/ios? "Courier" "monospace"))
|
||||
|
||||
(def code-block-background "#2E386B")
|
||||
|
||||
(def inline-code-style
|
||||
(update default-text-style :style
|
||||
assoc
|
||||
:font-family monospace-fonts
|
||||
:color colors/white
|
||||
:background-color code-block-background))
|
||||
|
||||
(def codeblock-style {:style {:padding 10
|
||||
:background-color code-block-background
|
||||
:border-radius 4}})
|
||||
|
||||
(def codeblock-text-style
|
||||
(update default-text-style :style
|
||||
assoc
|
||||
:font-family monospace-fonts
|
||||
:color colors/white))
|
||||
|
||||
(def default-blockquote-style
|
||||
{:style {:border-left-width 2
|
||||
:padding-left 3
|
||||
:border-left-color colors/gray-transparent-40}})
|
||||
|
||||
(def outgoing-blockquote-style
|
||||
(update default-blockquote-style :style
|
||||
assoc
|
||||
:border-left-color colors/white-transparent))
|
||||
|
||||
(defn blockquote-style [outgoing]
|
||||
(if outgoing
|
||||
outgoing-blockquote-style
|
||||
default-blockquote-style))
|
||||
|
||||
(def default-blockquote-text-style
|
||||
(update default-text-style :style
|
||||
assoc
|
||||
:line-height 19
|
||||
:font-size 14
|
||||
:color colors/black-transparent-50))
|
||||
|
||||
(def outgoing-blockquote-text-style
|
||||
(update default-blockquote-text-style :style
|
||||
assoc
|
||||
:color colors/white-transparent-70))
|
||||
|
||||
(defn blockquote-text-style [outgoing]
|
||||
(if outgoing
|
||||
outgoing-blockquote-text-style
|
||||
default-blockquote-text-style))
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
data))
|
||||
|
||||
;; Links starting with javascript:// should not be handled at all
|
||||
(def javascript-link-regex #"javascript://.*")
|
||||
(def javascript-link-regex #"(?i)javascript://.*")
|
||||
;; Anything with rtlo character we don't handle as it might be a spoofed url
|
||||
(def rtlo-link-regex #".*\u202e.*")
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' instead",
|
||||
"owner": "status-im",
|
||||
"repo": "status-go",
|
||||
"version": "v0.34.0-beta.7",
|
||||
"commit-sha1": "89659f85b49b48f4409ed9f522397b99728b214b",
|
||||
"src-sha256": "0kyk3r2wl3qxz28ifgnk2r8lh5116q8s58pk9x044dsrl0zvn5qv"
|
||||
"version": "v0.34.0-beta.8",
|
||||
"commit-sha1": "9d7c570593b1f88e9688204d8e1041d57b98da1f",
|
||||
"src-sha256": "1kwxf0h80vdj493c7s3lpmdjpbc72plbll0rj1vv7kzf8a84ff2k"
|
||||
}
|
||||
|
|
|
@ -207,7 +207,7 @@ class TestMessagesOneToOneChatMultiple(MultipleDeviceTestCase):
|
|||
home_2.home_button.click()
|
||||
|
||||
chat_1 = home_1.add_contact(public_key_2)
|
||||
url_message = 'status.im'
|
||||
url_message = 'http://status.im'
|
||||
chat_1.chat_message_input.send_keys(url_message)
|
||||
chat_1.send_message_button.click()
|
||||
chat_1.get_back_to_home_view()
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
(ns status-im.test.chat.models.message-content
|
||||
(:require [cljs.test :refer-macros [deftest is testing]]
|
||||
[status-im.utils.platform :as platform]
|
||||
[status-im.chat.models.message-content :as message-content]))
|
||||
|
||||
(deftest enrich-string-content-test
|
||||
(if platform/desktop?
|
||||
(testing "Text content of the message is enriched correctly"
|
||||
(is (not (:metadata (message-content/enrich-content {:text "Plain message"}))))
|
||||
(is (= {:bold [[5 14]]}
|
||||
(: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"}))))))
|
||||
|
||||
(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 (:render-recipe (message-content/enrich-content {:text "Plain message"}))))
|
||||
(is (= (if platform/desktop?
|
||||
'(["Test " :text]
|
||||
["#status" :tag]
|
||||
[" one three " :text]
|
||||
["#core-chat (@developer)!" :bold]
|
||||
[" By the way, " :text]
|
||||
["nice link(https://link.com)" :italic])
|
||||
'(["Test " :text]
|
||||
["#status" :tag]
|
||||
[" one three *" :text]
|
||||
["#core-chat" :tag]
|
||||
[" (" :text]
|
||||
["@developer" :mention]
|
||||
[")!* By the way, ~nice link(" :text]
|
||||
["https://link.com" :link]
|
||||
[")~" :text]))
|
||||
(:render-recipe (message-content/enrich-content {:text "Test #status one three *#core-chat (@developer)!* By the way, ~nice link(https://link.com)~"}))))))
|
|
@ -7,7 +7,6 @@
|
|||
[status-im.test.chat.commands.input]
|
||||
[status-im.test.chat.db]
|
||||
[status-im.test.chat.models.input]
|
||||
[status-im.test.chat.models.message-content]
|
||||
[status-im.test.chat.models.message]
|
||||
[status-im.test.chat.models.message-list]
|
||||
[status-im.test.chat.models]
|
||||
|
@ -87,7 +86,6 @@
|
|||
'status-im.test.chat.models.input
|
||||
'status-im.test.chat.models.message
|
||||
'status-im.test.chat.models.message-list
|
||||
'status-im.test.chat.models.message-content
|
||||
'status-im.test.chat.views.photos
|
||||
'status-im.test.transport.filters.core
|
||||
'status-im.test.contacts.db
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
(deftest safe-link-test-exceptions
|
||||
(testing "a javascript link"
|
||||
(is (not (security/safe-link? "javascript://anything"))))
|
||||
(testing "a javascript link mixed cases"
|
||||
(is (not (security/safe-link? "JaVasCrIpt://anything"))))
|
||||
(testing "a javascript link upper cases"
|
||||
(is (not (security/safe-link? "JAVASCRIPT://anything"))))
|
||||
(testing "rtlo links"
|
||||
(is (not (security/safe-link? rtlo-link)))))
|
||||
|
||||
|
|
Loading…
Reference in New Issue