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:
Andrea Maria Piana 2019-11-07 14:41:37 +01:00
parent 3127f2fcb2
commit 9a9c0ce526
No known key found for this signature in database
GPG Key ID: AA6CCA6DE0E06424
20 changed files with 338 additions and 170 deletions

View File

@ -589,6 +589,8 @@ var TopLevel = {
"prev": function() {}, "prev": function() {},
"hasNext": function() {}, "hasNext": function() {},
"hasPrev": function() {}, "hasPrev": function() {},
"rtl": function() {},
"lineCount": function() {},
"key": function() {}, "key": function() {},
"keys": function() {}, "keys": function() {},
"values": function() {}, "values": function() {},

View File

@ -7,6 +7,7 @@
[status-im.chat.commands.sending :as commands.sending] [status-im.chat.commands.sending :as commands.sending]
[status-im.chat.constants :as chat.constants] [status-im.chat.constants :as chat.constants]
[status-im.chat.models :as chat] [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.chat.models.message :as chat.message]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.js-dependencies :as dependencies] [status-im.js-dependencies :as dependencies]
@ -125,11 +126,15 @@
(let [{:keys [message-id]} (let [{:keys [message-id]}
(get-in db [:chats current-chat-id :metadata :responding-to-message]) (get-in db [:chats current-chat-id :metadata :responding-to-message])
show-name? (get-in db [:multiaccount :show-name?]) 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 (fx/merge cofx
{:db (assoc-in db [:chats current-chat-id :metadata :responding-to-message] nil)} {:db (assoc-in db [:chats current-chat-id :metadata :responding-to-message] nil)}
(chat.message/send-message {:chat-id current-chat-id (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 :content (cond-> {:chat-id current-chat-id
:text input-text} :text input-text}
message-id message-id
@ -140,15 +145,6 @@
(set-chat-input-text nil) (set-chat-input-text nil)
(process-cooldown))))) (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 (fx/defn send-sticker-fx
[{:keys [db] :as cofx} {:keys [hash pack]} current-chat-id] [{:keys [db] :as cofx} {:keys [hash pack]} current-chat-id]
(when-not (string/blank? hash) (when-not (string/blank? hash)

View File

@ -2,6 +2,7 @@
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[status-im.multiaccounts.model :as multiaccounts.model] [status-im.multiaccounts.model :as multiaccounts.model]
[status-im.chat.commands.receiving :as commands-receiving] [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.db :as chat.db]
[status-im.chat.models :as chat-model] [status-im.chat.models :as chat-model]
[status-im.chat.models.loading :as chat-loading] [status-im.chat.models.loading :as chat-loading]
@ -37,17 +38,14 @@
(defn- prepare-message (defn- prepare-message
[{:keys [content content-type] :as message} chat-id current-chat?] [{:keys [content content-type] :as message} chat-id current-chat?]
(let [emoji? (message-content/emoji-only-content? content)] (cond-> message
;; TODO janherich: enable the animations again once we can do them more efficiently current-chat?
(cond-> message (assoc :seen true) (and (= constants/content-type-text content-type)
current-chat? (message-content/should-collapse?
(assoc :seen true) (:text content)
(:line-count content)))
emoji? (assoc :should-collapse? true)))
(assoc :content-type constants/content-type-emoji)
(and (= constants/content-type-text content-type) (not emoji?))
(update :content message-content/enrich-content))))
(defn system-message? [{:keys [message-type]}] (defn system-message? [{:keys [message-type]}]
(= :system-message message-type)) (= :system-message message-type))
@ -250,24 +248,46 @@
:message message :message message
:current-chat? (= (get-in cofx [:db :current-chat-id]) chat-id)}))) :current-chat? (= (get-in cofx [:db :current-chat-id]) chat-id)})))
(fx/defn upsert-and-send (fx/defn prepare-message-content [cofx chat-id message]
[{:keys [now] :as cofx} {:keys [chat-id from] :as message}] {::json-rpc/call
(let [message (remove-icon message) [{:method "shhext_prepareContent"
send-record (protocol/map->Message (select-keys message transport-keys)) :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) wrapped-record (if (= (:message-type send-record) :group-user-message)
(wrap-group-message cofx chat-id send-record) (wrap-group-message cofx chat-id send-record)
send-record) send-record)]
message (assoc message :outgoing-status :sending)]
(fx/merge cofx (fx/merge cofx
(chat-model/upsert-chat (chat-model/upsert-chat
{:chat-id chat-id {:chat-id chat-id
:timestamp now :timestamp now
:last-message-timestamp (:timestamp message) :last-message-timestamp (:timestamp message-with-content)
:last-message-content (:content message) :last-message-content (:content message-with-content)
:last-message-content-type (:content-type message) :last-message-content-type (:content-type message-with-content)
:last-clock-value (:clock-value message)}) :last-clock-value (:clock-value message-with-content)})
(send chat-id message wrapped-record)))) (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 (fx/defn update-message-status
[{:keys [db] :as cofx} chat-id message-id status] [{:keys [db] :as cofx} chat-id message-id status]

View File

@ -40,9 +40,9 @@
(and (seq text) (and (seq text)
(re-matches constants/regx-rtl-characters (first 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)) (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] (defn- sorted-ranges [{:keys [metadata text]} metadata-keys]
(->> (if metadata-keys (->> (if metadata-keys
@ -81,28 +81,6 @@
(cond-> builder (cond-> builder
end-record (conj end-record)))))) 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? (defn emoji-only-content?
"Determines if text is just an emoji" "Determines if text is just an emoji"
[{:keys [text response-to]}] [{:keys [text response-to]}]

View File

@ -51,6 +51,7 @@
"shhext_chatMessages" {} "shhext_chatMessages" {}
"shhext_saveChat" {} "shhext_saveChat" {}
"shhext_contacts" {} "shhext_contacts" {}
"shhext_prepareContent" {}
"shhext_blockContact" {} "shhext_blockContact" {}
;;TODO not used anywhere? ;;TODO not used anywhere?
"shhext_deleteChat" {} "shhext_deleteChat" {}

View File

@ -616,11 +616,6 @@
{:db (assoc db :view-id view-id)} {:db (assoc db :view-id view-id)}
#(mark-messages-seen %)))) #(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 (handlers/register-handler-fx
:chat/send-sticker :chat/send-sticker
(fn [{{:keys [current-chat-id multiaccount]} :db :as cofx} [_ {:keys [hash] :as sticker}]] (fn [{{:keys [current-chat-id multiaccount]} :db :as cofx} [_ {:keys [hash] :as sticker}]]

View File

@ -3,7 +3,9 @@
(:require [clojure.set :as clojure.set] (:require [clojure.set :as clojure.set]
[clojure.spec.alpha :as spec] [clojure.spec.alpha :as spec]
[clojure.string :as string] [clojure.string :as string]
[status-im.ethereum.json-rpc :as json-rpc]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[status-im.chat.models.message-content :as message-content]
[status-im.multiaccounts.core :as multiaccounts] [status-im.multiaccounts.core :as multiaccounts]
[status-im.multiaccounts.model :as multiaccounts.model] [status-im.multiaccounts.model :as multiaccounts.model]
[status-im.utils.pairing :as pairing.utils] [status-im.utils.pairing :as pairing.utils]
@ -463,6 +465,43 @@
(transport.filters/upsert-group-chat-topics) (transport.filters/upsert-group-chat-topics)
(transport.filters/load-members members))))) (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 (fx/defn handle-membership-update
"Upsert chat and receive message if valid" "Upsert chat and receive message if valid"
;; Care needs to be taken here as chat-id is not coming from a whisper filter ;; 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 ;; don't allow anything but group messages
(instance? protocol/Message message) (instance? protocol/Message message)
(= :group-user-message (:message-type message))) (= :group-user-message (:message-type message)))
(protocol/receive message (prepare-message-content
chat-id %
sender-signature chat-id
whisper-timestamp message
(assoc % :metadata metadata)))))))) sender-signature
whisper-timestamp
metadata)))))))
(defn handle-sign-success (defn handle-sign-success
"Upsert chat and send signed payload to group members" "Upsert chat and send signed payload to group members"

View File

@ -505,9 +505,9 @@
(re-frame/reg-sub (re-frame/reg-sub
::show-suggestions-view? ::show-suggestions-view?
:<- [:chats/current-chat-ui-prop :show-suggestions?] :<- [:chats/current-chat-ui-prop :show-suggestions?]
:<- [:chats/current-chat] :<- [:chats/current-chat-input-text]
:<- [:chats/all-available-commands] :<- [:chats/all-available-commands]
(fn [[show-suggestions? {:keys [input-text]} commands]] (fn [[show-suggestions? input-text commands]]
(and (or show-suggestions? (and (or show-suggestions?
(commands.input/starts-as-command? (string/trim (or input-text "")))) (commands.input/starts-as-command? (string/trim (or input-text ""))))
(seq commands)))) (seq commands))))
@ -523,7 +523,7 @@
::get-commands-for-chat ::get-commands-for-chat
:<- [:chats/id->command] :<- [:chats/id->command]
:<- [::access-scope->command-id] :<- [::access-scope->command-id]
:<- [:chats/current-chat] :<- [:chats/current-raw-chat]
(fn [[id->command access-scope->command-id chat]] (fn [[id->command access-scope->command-id chat]]
(commands/chat-commands id->command access-scope->command-id chat))) (commands/chat-commands id->command access-scope->command-id chat)))
@ -666,9 +666,21 @@
:messages)))) :messages))))
(re-frame/reg-sub (re-frame/reg-sub
:chats/current-chat :chats/current-raw-chat
:<- [:chats/active-chats] :<- [:chats/active-chats]
:<- [:chats/current-chat-id] :<- [: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] :<- [:multiaccount/public-key]
:<- [:mailserver/ranges] :<- [:mailserver/ranges]
:<- [:chats/content-layout-height] :<- [:chats/content-layout-height]
@ -677,27 +689,28 @@
:<- [:ethereum/chain-keyword] :<- [:ethereum/chain-keyword]
:<- [:prices] :<- [:prices]
:<- [:wallet/currency] :<- [: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]] input-height ttt-settings chain-keyword prices currency]]
(let [{:keys [group-chat contact messages] (when current-chat
:as current-chat} (cond-> (enrich-current-chat current-chat ranges height input-height)
(get chats current-chat-id)] (empty? messages)
(when current-chat (assoc :universal-link
(cond-> (enrich-current-chat current-chat ranges height input-height) (links/generate-link :public-chat :external chat-id))
(empty? messages)
(assoc :universal-link
(links/generate-link :public-chat :external current-chat-id))
(chat.models/public-chat? current-chat) (chat.models/public-chat? current-chat)
(assoc :show-input? true) (assoc :show-input? true)
(and (chat.models/group-chat? current-chat) (and (chat.models/group-chat? current-chat)
(group-chats.db/joined? my-public-key current-chat)) (group-chats.db/joined? my-public-key current-chat))
(assoc :show-input? true) (assoc :show-input? true)
(not group-chat) (not group-chat)
(enrich-current-one-to-one-chat my-public-key ttt-settings (enrich-current-one-to-one-chat my-public-key ttt-settings
chain-keyword prices currency)))))) chain-keyword prices currency)))))
(re-frame/reg-sub (re-frame/reg-sub
:chats/current-chat-message :chats/current-chat-message
@ -798,10 +811,10 @@
(re-frame/reg-sub (re-frame/reg-sub
:chats/selected-chat-command :chats/selected-chat-command
:<- [:chats/current-chat] :<- [:chats/current-chat-input-text]
:<- [:chats/current-chat-ui-prop :selection] :<- [:chats/current-chat-ui-prop :selection]
:<- [::get-commands-for-chat] :<- [::get-commands-for-chat]
(fn [[{:keys [input-text]} selection commands]] (fn [[input-text selection commands]]
(commands.input/selected-chat-command input-text selection commands))) (commands.input/selected-chat-command input-text selection commands)))
(re-frame/reg-sub (re-frame/reg-sub

View File

@ -59,6 +59,7 @@
(spec/def :message.content/params (spec/map-of keyword? any?)) (spec/def :message.content/params (spec/map-of keyword? any?))
(spec/def ::content-type #{constants/content-type-text constants/content-type-command (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}) constants/content-type-command-request constants/content-type-sticker})
(spec/def ::message-type #{:group-user-message :public-group-user-message :user-message}) (spec/def ::message-type #{:group-user-message :public-group-user-message :user-message})
(spec/def ::clock-value (spec/and pos-int? (spec/def ::clock-value (spec/and pos-int?

View File

@ -12,6 +12,7 @@
[status-im.transport.message.transit :as transit] [status-im.transport.message.transit :as transit]
[status-im.transport.utils :as transport.utils] [status-im.transport.utils :as transport.utils]
[status-im.tribute-to-talk.whitelist :as whitelist] [status-im.tribute-to-talk.whitelist :as whitelist]
[cljs-bean.core :as clj-bean]
[status-im.utils.config :as config] [status-im.utils.config :as config]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[taoensso.timbre :as log] [taoensso.timbre :as log]
@ -19,14 +20,20 @@
(def message-type-message 1) (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] (defn build-message [parsed-message-js]
(let [content (.-content parsed-message-js) (let [content (.-content parsed-message-js)
built-message built-message
(protocol/Message. (protocol/Message.
{:text (.-text content) (build-content content)
:response-to (aget content "response-to")
:name (.-name content)
:chat-id (.-chat_id content)}
(.-content_type parsed-message-js) (.-content_type parsed-message-js)
(keyword (.-message_type parsed-message-js)) (keyword (.-message_type parsed-message-js))
(.-clock parsed-message-js) (.-clock parsed-message-js)

View File

@ -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 (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-20 (alpha black 0.2)) ; accounts divider
(def black-transparent-40 (alpha black 0.4)) (def black-transparent-40 (alpha black 0.4))
(def black-transparent-50 (alpha black 0.5))
(def black-light "#2d2d2d") ;; sign-with-keycard-button (def black-light "#2d2d2d") ;; sign-with-keycard-button
;; DARK GREY ;; DARK GREY

View File

@ -24,7 +24,7 @@
[status-im.ui.screens.chat.stickers.views :as stickers])) [status-im.ui.screens.chat.stickers.views :as stickers]))
(defview basic-text-input [{:keys [set-container-width-fn height single-line-input?]}] (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?]] cooldown-enabled? [:chats/cooldown-enabled?]]
[react/text-input [react/text-input
(merge (merge
@ -91,7 +91,7 @@
{:placeholder (i18n/label :cooldown/text-input-disabled)}))])) {:placeholder (i18n/label :cooldown/text-input-disabled)}))]))
(defview invisible-input [{:keys [set-layout-width-fn value]}] (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 [react/text {:style style/invisible-input-text
:on-layout #(let [w (-> (.-nativeEvent %) :on-layout #(let [w (-> (.-nativeEvent %)
(.-layout) (.-layout)
@ -184,7 +184,7 @@
(defview input-container [] (defview input-container []
(letsubs [margin [:chats/input-margin] (letsubs [margin [:chats/input-margin]
mainnet? [:mainnet?] mainnet? [:mainnet?]
{:keys [input-text]} [:chats/current-chat] input-text [:chats/current-chat-input-text]
result-box [:chats/current-chat-ui-prop :result-box] result-box [:chats/current-chat-ui-prop :result-box]
show-stickers? [:chats/current-chat-ui-prop :show-stickers?] show-stickers? [:chats/current-chat-ui-prop :show-stickers?]
state-text (reagent/atom "")] state-text (reagent/atom "")]

View File

@ -2,8 +2,11 @@
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[status-im.chat.commands.receiving :as commands-receiving] [status-im.chat.commands.receiving :as commands-receiving]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.utils.http :as http]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[reagent.core :as reagent]
[status-im.ui.components.colors :as colors] [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.icons.vector-icons :as vector-icons]
[status-im.ui.components.list-selection :as list-selection] [status-im.ui.components.list-selection :as list-selection]
[status-im.ui.components.popup-menu.views :as desktop.pop-up] [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])} :on-press #(re-frame/dispatch [:chat.ui/message-expand-toggled chat-id message-id])}
(i18n/label (if expanded? :show-less :show-more))]) (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 (defn text-message
[{:keys [chat-id message-id content [{:keys [chat-id message-id content
timestamp-str group-chat outgoing current-public-key expanded?] :as message}] timestamp-str group-chat outgoing current-public-key expanded?] :as message}]
[message-view message [message-view message
(let [response-to (:response-to content) (let [response-to (:response-to content)]
collapsible? (and (:should-collapse? content) group-chat)]
[react/view [react/view
(when response-to (when (seq response-to)
[quoted-message response-to (:quoted-message message) outgoing current-public-key]) [quoted-message response-to (:quoted-message message) outgoing current-public-key])
(apply react/nested-text [render-parsed-text message (:parsed-text content)]])
(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])])
{:justify-timestamp? true}]) {:justify-timestamp? true}])
(defn emoji-message (defn emoji-message

View File

@ -1,8 +1,10 @@
(ns status-im.ui.screens.chat.styles.message.message (ns status-im.ui.screens.chat.styles.message.message
(:require [status-im.constants :as constants] (:require [status-im.constants :as constants]
[status-im.ui.components.colors :as colors] [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.ui.screens.chat.styles.photos :as photos]
[status-im.utils.platform :as platform] [status-im.utils.platform :as platform]
[status-im.ui.components.typography :as typography]
[status-im.utils.styles :as styles])) [status-im.utils.styles :as styles]))
(defn style-message-text (defn style-message-text
@ -216,3 +218,98 @@
:color (if outgoing :color (if outgoing
colors/white-transparent-70 colors/white-transparent-70
colors/gray)}) 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))

View File

@ -24,7 +24,7 @@
data)) data))
;; Links starting with javascript:// should not be handled at all ;; 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 ;; Anything with rtlo character we don't handle as it might be a spoofed url
(def rtlo-link-regex #".*\u202e.*") (def rtlo-link-regex #".*\u202e.*")

View File

@ -2,7 +2,7 @@
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' instead", "_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' instead",
"owner": "status-im", "owner": "status-im",
"repo": "status-go", "repo": "status-go",
"version": "v0.34.0-beta.7", "version": "v0.34.0-beta.8",
"commit-sha1": "89659f85b49b48f4409ed9f522397b99728b214b", "commit-sha1": "9d7c570593b1f88e9688204d8e1041d57b98da1f",
"src-sha256": "0kyk3r2wl3qxz28ifgnk2r8lh5116q8s58pk9x044dsrl0zvn5qv" "src-sha256": "1kwxf0h80vdj493c7s3lpmdjpbc72plbll0rj1vv7kzf8a84ff2k"
} }

View File

@ -207,7 +207,7 @@ class TestMessagesOneToOneChatMultiple(MultipleDeviceTestCase):
home_2.home_button.click() home_2.home_button.click()
chat_1 = home_1.add_contact(public_key_2) 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.chat_message_input.send_keys(url_message)
chat_1.send_message_button.click() chat_1.send_message_button.click()
chat_1.get_back_to_home_view() chat_1.get_back_to_home_view()

View File

@ -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)~"}))))))

View File

@ -7,7 +7,6 @@
[status-im.test.chat.commands.input] [status-im.test.chat.commands.input]
[status-im.test.chat.db] [status-im.test.chat.db]
[status-im.test.chat.models.input] [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]
[status-im.test.chat.models.message-list] [status-im.test.chat.models.message-list]
[status-im.test.chat.models] [status-im.test.chat.models]
@ -87,7 +86,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-list 'status-im.test.chat.models.message-list
'status-im.test.chat.models.message-content
'status-im.test.chat.views.photos 'status-im.test.chat.views.photos
'status-im.test.transport.filters.core 'status-im.test.transport.filters.core
'status-im.test.contacts.db 'status-im.test.contacts.db

View File

@ -16,6 +16,10 @@
(deftest safe-link-test-exceptions (deftest safe-link-test-exceptions
(testing "a javascript link" (testing "a javascript link"
(is (not (security/safe-link? "javascript://anything")))) (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" (testing "rtlo links"
(is (not (security/safe-link? rtlo-link))))) (is (not (security/safe-link? rtlo-link)))))