Emoji reactions

Signed-off-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
This commit is contained in:
Gheorghe Pinzaru 2020-07-03 18:04:45 +03:00 committed by Andrea Maria Piana
parent 933c0f8f8c
commit 1c308c2d01
No known key found for this signature in database
GPG Key ID: AA6CCA6DE0E06424
54 changed files with 846 additions and 271 deletions

View File

@ -59,6 +59,7 @@
"react-native-svg": "^9.8.4", "react-native-svg": "^9.8.4",
"react-native-touch-id": "^4.4.1", "react-native-touch-id": "^4.4.1",
"react-native-webview": "git+https://github.com/status-im/react-native-webview.git#v10.2.3_5", "react-native-webview": "git+https://github.com/status-im/react-native-webview.git#v10.2.3_5",
"tdigest": "^0.1.1",
"web3-utils": "^1.2.1" "web3-utils": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -64,6 +64,19 @@
:ease-in-out (bezier 0.42 0 0.58 1) :ease-in-out (bezier 0.42 0 0.58 1)
:keyboard (bezier 0.17 0.59 0.4 0.77)}) :keyboard (bezier 0.17 0.59 0.4 0.77)})
(def springs {:lazy {:damping 50
:mass 0.3
:stiffness 120
:overshootClamping true
:bouncyFactor 1}
:jump {:damping 13
:mass 0.5
:stiffness 170
:overshootClamping false
:bouncyFactor 1
:restSpeedThreshold 0.001
:restDisplacementThreshold 0.001}})
(defn set-value [anim val] (defn set-value [anim val]
(ocall anim "setValue" val)) (ocall anim "setValue" val))

View File

@ -8,12 +8,6 @@
[quo.components.controls.styles :as styles] [quo.components.controls.styles :as styles]
[status-im.ui.components.icons.vector-icons :as icons])) [status-im.ui.components.icons.vector-icons :as icons]))
(def spring-config {:damping 50
:mass 0.3
:stiffness 120
:overshootClamping true
:bouncyFactor 1})
(defn control-builder [component] (defn control-builder [component]
(fn [props] (fn [props]
(let [{:keys [value onChange disabled]} (let [{:keys [value onChange disabled]}
@ -29,7 +23,7 @@
[]) [])
transition (react/use-memo transition (react/use-memo
(fn [] (fn []
(animated/with-spring-transition state spring-config)) (animated/with-spring-transition state (:lazy animated/springs)))
[]) [])
press-end (fn [] press-end (fn []
(when (and (not disabled) onChange) (when (and (not disabled) onChange)

View File

@ -168,7 +168,7 @@
(not on-long-press)) (not on-long-press))
rn/view rn/view
animated animated/pressable animated animated/pressable
:else gh/touchable-hightlight)] :else gh/touchable-highlight)]
[rn/view {:background-color (if (and (= accessory :radio) active) [rn/view {:background-color (if (and (= accessory :radio) active)
active-background active-background
passive-background)} passive-background)}

View File

@ -169,7 +169,7 @@
:on-press #(reset! visible true)} :on-press #(reset! visible true)}
:else after) :else after)
secure (and secure-text-entry secure (and (true? secure-text-entry)
(or platform/android? (not @visible))) (or platform/android? (not @visible)))
on-cancel (fn [] on-cancel (fn []
(when on-cancel (when on-cancel

View File

@ -32,11 +32,11 @@
(def touchable-without-feedback (def touchable-without-feedback
(reagent/adapt-react-class touchable-without-feedback-class)) (reagent/adapt-react-class touchable-without-feedback-class))
(def touchable-hightlight-class (reagent/adapt-react-class TouchableHighlight)) (def touchable-highlight-class (reagent/adapt-react-class TouchableHighlight))
(defn touchable-hightlight [props & children] (defn touchable-highlight [props & children]
(into [touchable-hightlight-class (merge {:underlay-color (:interactive-02 @colors/theme)} (into [touchable-highlight-class (merge {:underlay-color (:interactive-02 @colors/theme)}
props)] props)]
children)) children))
(def touchable-opacity (def touchable-opacity

View File

@ -8,6 +8,7 @@
[status-im.transport.filters.core :as filters] [status-im.transport.filters.core :as filters]
[status-im.mailserver.core :as mailserver] [status-im.mailserver.core :as mailserver]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[status-im.chat.models.reactions :as reactions]
[status-im.chat.models.message-list :as message-list] [status-im.chat.models.message-list :as message-list]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[status-im.chat.models.message-seen :as message-seen])) [status-im.chat.models.message-seen :as message-seen]))
@ -133,6 +134,7 @@
#(re-frame/dispatch [::failed-loading-messages current-chat-id session-id %]))] #(re-frame/dispatch [::failed-loading-messages current-chat-id session-id %]))]
(fx/merge cofx (fx/merge cofx
load-messages-fx load-messages-fx
(reactions/load-more-reactions cursor)
(mailserver/load-gaps-fx current-chat-id))))))) (mailserver/load-gaps-fx current-chat-id)))))))
(fx/defn load-messages (fx/defn load-messages

View File

@ -0,0 +1,95 @@
(ns status-im.chat.models.reactions
(:require [status-im.constants :as constants]
[re-frame.core :as re-frame]
[status-im.utils.fx :as fx]
[status-im.waku.core :as waku]
[taoensso.timbre :as log]
[status-im.transport.message.protocol :as message.protocol]
[status-im.data-store.reactions :as data-store.reactions]))
(defn process-reactions
[reactions new-reactions]
;; TODO(Ferossgp): handling own reaction in subscription could be expensive,
;; for better performance we can here separate own reaction into 2 maps
(reduce
(fn [acc {:keys [chat-id message-id emoji-id emoji-reaction-id retracted]
:as reaction}]
;; NOTE(Ferossgp): For a better performance, better to not keep in db all retracted reactions
;; retraction will always come after the reaction so there shouldn't be a conflict
(if retracted
(update-in acc [chat-id message-id emoji-id] dissoc emoji-reaction-id)
(assoc-in acc [chat-id message-id emoji-id emoji-reaction-id] reaction)))
reactions
new-reactions))
(defn- earlier-than-deleted-at?
[{:keys [db]} {:keys [chat-id clock-value]}]
(let [{:keys [deleted-at-clock-value]}
(get-in db [:chats chat-id])]
(>= deleted-at-clock-value clock-value)))
(fx/defn receive-signal
[{:keys [db] :as cofx} reactions]
(let [reactions (filter (partial earlier-than-deleted-at? cofx) reactions)]
{:db (update db :reactions process-reactions reactions)}))
(fx/defn load-more-reactions
[{:keys [db] :as cofx} cursor]
(when-let [current-chat-id (:current-chat-id db)]
(when-let [session-id (get-in db [:pagination-info current-chat-id :messages-initialized?])]
(data-store.reactions/reactions-by-chat-id-rpc
(waku/enabled? cofx)
current-chat-id
cursor
constants/default-number-of-messages
#(re-frame/dispatch [::reactions-loaded current-chat-id session-id %])
#(log/error "failed loading reactions" current-chat-id %)))))
(fx/defn reactions-loaded
{:events [::reactions-loaded]}
[{{:keys [current-chat-id] :as db} :db}
chat-id
session-id
reactions]
(when-not (or (nil? current-chat-id)
(not= chat-id current-chat-id)
(and (get-in db [:pagination-info current-chat-id :messages-initialized?])
(not= session-id
(get-in db [:pagination-info current-chat-id :messages-initialized?]))))
(let [reactions-w-chat-id (map #(assoc % :chat-id chat-id) reactions)]
{:db (update db :reactions process-reactions reactions-w-chat-id)})))
;; Send reactions
(fx/defn send-emoji-reaction
{:events [::send-emoji-reaction]}
[{{:keys [current-chat-id] :as db} :db :as cofx} reaction]
(message.protocol/send-reaction cofx
(assoc reaction :chat-id current-chat-id)))
(fx/defn send-retract-emoji-reaction
{:events [::send-emoji-reaction-retraction]}
[{{:keys [current-chat-id reactions] :as db} :db :as cofx} reaction]
(message.protocol/send-retract-reaction cofx
(assoc reaction :chat-id current-chat-id)))
(fx/defn receive-one
{:events [::receive-one]}
[{:keys [db]} reaction]
{:db (update db :reactions process-reactions [reaction])})
(defn message-reactions [current-public-key reactions]
(reduce
(fn [acc [emoji-id reactions]]
(if (pos? (count reactions))
(let [own (first (filter (fn [[_ {:keys [from]}]]
(= from current-public-key)) reactions))]
(conj acc {:emoji-id emoji-id
:own (boolean (seq own))
:emoji-reaction-id (:emoji-reaction-id (second own))
:quantity (count reactions)}))
acc))
[]
reactions))

View File

@ -1,5 +1,6 @@
(ns status-im.constants (ns status-im.constants
(:require [status-im.ethereum.core :as ethereum] (:require [status-im.ethereum.core :as ethereum]
[status-im.react-native.resources :as resources]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.utils.config :as config])) [status-im.utils.config :as config]))
@ -16,6 +17,20 @@
(def content-type-image 7) (def content-type-image 7)
(def content-type-audio 8) (def content-type-audio 8)
(def emoji-reaction-love 1)
(def emoji-reaction-thumbs-up 2)
(def emoji-reaction-thumbs-down 3)
(def emoji-reaction-laugh 4)
(def emoji-reaction-sad 5)
(def emoji-reaction-angry 6)
(def reactions {emoji-reaction-love (:love resources/reactions)
emoji-reaction-thumbs-up (:thumbs-up resources/reactions)
emoji-reaction-thumbs-down (:thumbs-down resources/reactions)
emoji-reaction-laugh (:laugh resources/reactions)
emoji-reaction-sad (:sad resources/reactions)
emoji-reaction-angry (:angry resources/reactions)})
(def message-type-one-to-one 1) (def message-type-one-to-one 1)
(def message-type-public-group 2) (def message-type-public-group 2)
(def message-type-private-group 3) (def message-type-private-group 3)

View File

@ -62,7 +62,7 @@
(.addListener ^js react/keyboard (.addListener ^js react/keyboard
(if platform/ios? (if platform/ios?
"keyboardWillHide" "keyboardWillHide"
"keyboardWDidHide") "keyboardDidHide")
(fn [_] (fn [_]
(re-frame/dispatch-sync [:set :keyboard-height 0]))) (re-frame/dispatch-sync [:set :keyboard-height 0])))
(.addEventListener ^js react/app-state "change" app-state-change-handler) (.addEventListener ^js react/app-state "change" app-state-change-handler)

View File

@ -0,0 +1,32 @@
(ns status-im.data-store.reactions
(:require [clojure.set :as clojure.set]
[status-im.ethereum.json-rpc :as json-rpc]))
(defn ->rpc [message]
(-> message
(clojure.set/rename-keys {:message-id :messageId
:emoji-id :emojiId
:chat-id :localChatId
:message-type :messageType
:emoji-reaction-id :id})))
(defn <-rpc [message]
(-> message
(dissoc :chat_id)
(clojure.set/rename-keys {:messageId :message-id
:localChatId :chat-id
:emojiId :emoji-id
:messageType :message-type
:id :emoji-reaction-id})))
(defn reactions-by-chat-id-rpc [waku-enabled?
chat-id
cursor
limit
on-success
on-failure]
{::json-rpc/call [{:method (json-rpc/call-ext-method waku-enabled? "emojiReactionsByChatID")
:params [chat-id cursor limit]
:on-success (fn [result]
(on-success (map <-rpc result)))
:on-failure on-failure}]})

View File

@ -74,6 +74,9 @@
"shhext_prepareContent" {} "shhext_prepareContent" {}
"shhext_blockContact" {} "shhext_blockContact" {}
"shhext_updateMailservers" {} "shhext_updateMailservers" {}
"shhext_sendEmojiReaction" {}
"shhext_sendEmojiReactionRetraction" {}
"shhext_emojiReactionsByChatID" {}
;;TODO not used anywhere? ;;TODO not used anywhere?
"shhext_deleteChat" {} "shhext_deleteChat" {}
"shhext_saveContact" {} "shhext_saveContact" {}
@ -125,6 +128,9 @@
"wakuext_prepareContent" {} "wakuext_prepareContent" {}
"wakuext_blockContact" {} "wakuext_blockContact" {}
"wakuext_updateMailservers" {} "wakuext_updateMailservers" {}
"wakuext_sendEmojiReaction" {}
"wakuext_sendEmojiReactionRetraction" {}
"wakuext_emojiReactionsByChatID" {}
;;TODO not used anywhere? ;;TODO not used anywhere?
"wakuext_deleteChat" {} "wakuext_deleteChat" {}
"wakuext_saveContact" {} "wakuext_saveContact" {}

View File

@ -7,11 +7,13 @@
[status-im.chat.models.input :as chat.input] [status-im.chat.models.input :as chat.input]
[status-im.chat.models.loading :as chat.loading] [status-im.chat.models.loading :as chat.loading]
[status-im.chat.models.message :as chat.message] [status-im.chat.models.message :as chat.message]
[status-im.chat.models.reactions :as chat.reactions]
[status-im.chat.models.message-seen :as message-seen] [status-im.chat.models.message-seen :as message-seen]
[status-im.contact.block :as contact.block] [status-im.contact.block :as contact.block]
[status-im.contact.core :as contact] [status-im.contact.core :as contact]
[status-im.data-store.chats :as data-store.chats] [status-im.data-store.chats :as data-store.chats]
[status-im.data-store.messages :as data-store.messages] [status-im.data-store.messages :as data-store.messages]
[status-im.data-store.reactions :as data-store.reactions]
[status-im.ethereum.subscriptions :as ethereum.subscriptions] [status-im.ethereum.subscriptions :as ethereum.subscriptions]
[status-im.fleet.core :as fleet] [status-im.fleet.core :as fleet]
[status-im.group-chats.core :as group-chats] [status-im.group-chats.core :as group-chats]
@ -794,12 +796,14 @@
(fn [_ [_ err]] (fn [_ [_ err]]
(log/error :send-status-message-error err))) (log/error :send-status-message-error err)))
(fx/defn handle-update [cofx {:keys [chats messages] :as response}] (fx/defn handle-update [cofx {:keys [chats messages emojiReactions] :as response}]
(let [chats (map data-store.chats/<-rpc chats) (let [chats (map data-store.chats/<-rpc chats)
messages (map data-store.messages/<-rpc messages) messages (map data-store.messages/<-rpc messages)
message-fxs (map chat.message/receive-one messages) message-fxs (map chat.message/receive-one messages)
chat-fxs (map #(chat/ensure-chat (dissoc % :unviewed-messages-count)) chats)] emoji-reactions (map data-store.reactions/<-rpc emojiReactions)
(apply fx/merge cofx (concat chat-fxs message-fxs)))) emoji-react-fxs (map chat.reactions/receive-one emoji-reactions)
chat-fxs (map #(chat/ensure-chat (dissoc % :unviewed-messages-count)) chats)]
(apply fx/merge cofx (concat chat-fxs message-fxs emoji-react-fxs))))
(handlers/register-handler-fx (handlers/register-handler-fx
:transport/message-sent :transport/message-sent
@ -811,6 +815,16 @@
(conj set-hash-fxs (conj set-hash-fxs
(handle-update response)))))) (handle-update response))))))
(fx/defn reaction-sent
{:events [:transport/reaction-sent]}
[cofx response]
(handle-update cofx response))
(fx/defn retraction-sent
{:events [:transport/retraction-sent]}
[cofx response]
(handle-update cofx response))
(handlers/register-handler-fx (handlers/register-handler-fx
:transport.callback/node-info-fetched :transport.callback/node-info-fetched
(fn [cofx [_ node-info]] (fn [cofx [_ node-info]]

View File

@ -51,3 +51,11 @@
(get @loaded-images k) (get @loaded-images k)
(get (swap! loaded-images assoc k (get (swap! loaded-images assoc k
(get ui k)) k))) (get ui k)) k)))
(def reactions
{:love (js/require "../resources/images/reactions/love.png")
:angry (js/require "../resources/images/reactions/angry.png")
:sad (js/require "../resources/images/reactions/sad.png")
:laugh (js/require "../resources/images/reactions/laugh.png")
:thumbs-up (js/require "../resources/images/reactions/thumbs-up.png")
:thumbs-down (js/require "../resources/images/reactions/thumbs-down.png")})

View File

@ -20,6 +20,7 @@
[status-im.multiaccounts.db :as multiaccounts.db] [status-im.multiaccounts.db :as multiaccounts.db]
[status-im.multiaccounts.model :as multiaccounts.model] [status-im.multiaccounts.model :as multiaccounts.model]
[status-im.multiaccounts.recover.core :as recover] [status-im.multiaccounts.recover.core :as recover]
[status-im.chat.models.reactions :as models.reactions]
[status-im.pairing.core :as pairing] [status-im.pairing.core :as pairing]
[status-im.signing.gas :as signing.gas] [status-im.signing.gas :as signing.gas]
#_[status-im.tribute-to-talk.core :as tribute-to-talk] #_[status-im.tribute-to-talk.core :as tribute-to-talk]
@ -182,6 +183,7 @@
(reg-root-key-sub :multiaccounts/loading :multiaccounts/loading) (reg-root-key-sub :multiaccounts/loading :multiaccounts/loading)
(reg-root-key-sub ::messages :messages) (reg-root-key-sub ::messages :messages)
(reg-root-key-sub ::reactions :reactions)
(reg-root-key-sub ::message-lists :message-lists) (reg-root-key-sub ::message-lists :message-lists)
(reg-root-key-sub ::pagination-info :pagination-info) (reg-root-key-sub ::pagination-info :pagination-info)
@ -637,6 +639,16 @@
(fn [[messages chat-id]] (fn [[messages chat-id]]
(get messages chat-id {}))) (get messages chat-id {})))
(re-frame/reg-sub
:chats/message-reactions
:<- [:multiaccount/public-key]
:<- [::reactions]
:<- [:chats/current-chat-id]
(fn [[current-public-key reactions chat-id] [_ message-id]]
(models.reactions/message-reactions
current-public-key
(get-in reactions [chat-id message-id]))))
(re-frame/reg-sub (re-frame/reg-sub
:chats/messages-gaps :chats/messages-gaps
:<- [:mailserver/gaps] :<- [:mailserver/gaps]

View File

@ -2,12 +2,13 @@
status-im.transport.message.core status-im.transport.message.core
(:require [status-im.chat.models.message :as models.message] (:require [status-im.chat.models.message :as models.message]
[status-im.chat.models :as models.chat] [status-im.chat.models :as models.chat]
[status-im.chat.models.reactions :as models.reactions]
[status-im.contact.core :as models.contact] [status-im.contact.core :as models.contact]
[status-im.pairing.core :as models.pairing] [status-im.pairing.core :as models.pairing]
[status-im.data-store.messages :as data-store.messages] [status-im.data-store.messages :as data-store.messages]
[status-im.data-store.reactions :as data-store.reactions]
[status-im.data-store.contacts :as data-store.contacts] [status-im.data-store.contacts :as data-store.contacts]
[status-im.data-store.chats :as data-store.chats] [status-im.data-store.chats :as data-store.chats]
[status-im.utils.handlers :as handlers]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[status-im.utils.types :as types])) [status-im.utils.types :as types]))
@ -20,11 +21,17 @@
(fx/defn handle-message [cofx message] (fx/defn handle-message [cofx message]
(models.message/receive-one cofx message)) (models.message/receive-one cofx message))
(fx/defn process-response [cofx ^js response-js] (fx/defn handle-reactions [cofx reactions]
(models.reactions/receive-signal cofx reactions))
(fx/defn process-response
{:events [::process]}
[cofx ^js response-js]
(let [^js chats (.-chats response-js) (let [^js chats (.-chats response-js)
^js contacts (.-contacts response-js) ^js contacts (.-contacts response-js)
^js installations (.-installations response-js) ^js installations (.-installations response-js)
^js messages (.-messages response-js)] ^js messages (.-messages response-js)
^js emoji-reactions (.-emojiReactions response-js)]
(cond (cond
(seq installations) (seq installations)
(let [installations-clj (types/js->clj installations)] (let [installations-clj (types/js->clj installations)]
@ -54,12 +61,14 @@
(let [message (.pop messages)] (let [message (.pop messages)]
(fx/merge cofx (fx/merge cofx
{:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]} {:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]}
(handle-message (-> message (types/js->clj) (data-store.messages/<-rpc)))))))) (handle-message (-> message (types/js->clj) (data-store.messages/<-rpc)))))
(handlers/register-handler-fx (seq emoji-reactions)
::process (let [reactions (types/js->clj emoji-reactions)]
(fn [cofx [_ response-js]] (js-delete response-js "emojiReactions")
(process-response cofx response-js))) (fx/merge cofx
{:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]}
(handle-reactions (map data-store.reactions/<-rpc reactions)))))))
(fx/defn remove-hash (fx/defn remove-hash
[{:keys [db] :as cofx} envelope-hash] [{:keys [db] :as cofx} envelope-hash]

View File

@ -31,3 +31,21 @@
:on-success :on-success
#(re-frame/dispatch [:transport/message-sent % 1]) #(re-frame/dispatch [:transport/message-sent % 1])
:on-failure #(log/error "failed to send a message" %)}]}) :on-failure #(log/error "failed to send a message" %)}]})
(fx/defn send-reaction [cofx {:keys [message-id chat-id emoji-id]}]
{::json-rpc/call [{:method (json-rpc/call-ext-method
(get-in cofx [:db :multiaccount :waku-enabled])
"sendEmojiReaction")
:params [chat-id message-id emoji-id]
:on-success
#(re-frame/dispatch [:transport/reaction-sent %])
:on-failure #(log/error "failed to send a reaction" %)}]})
(fx/defn send-retract-reaction [cofx {:keys [emoji-reaction-id] :as reaction}]
{::json-rpc/call [{:method (json-rpc/call-ext-method
(get-in cofx [:db :multiaccount :waku-enabled])
"sendEmojiReactionRetraction")
:params [emoji-reaction-id]
:on-success
#(re-frame/dispatch [:transport/retraction-sent %])
:on-failure #(log/error "failed to send a reaction retraction" %)}]})

View File

@ -134,14 +134,9 @@
:set-active set-active-panel}])]]]]) :set-active set-active-panel}])]]]])
(defn chat-toolbar [] (defn chat-toolbar []
(let [text-input-ref (react/create-ref) (let [previous-layout (atom nil)
input-focus (fn []
(some-> ^js (react/current-ref text-input-ref) .focus))
clear-input (fn []
(some-> ^js (react/current-ref text-input-ref) .clear))
previous-layout (atom nil)
had-reply (atom nil)] had-reply (atom nil)]
(fn [{:keys [active-panel set-active-panel]}] (fn [{:keys [active-panel set-active-panel text-input-ref]}]
(let [disconnected? @(re-frame/subscribe [:disconnected?]) (let [disconnected? @(re-frame/subscribe [:disconnected?])
{:keys [processing]} @(re-frame/subscribe [:multiaccounts/login]) {:keys [processing]} @(re-frame/subscribe [:multiaccounts/login])
mainnet? @(re-frame/subscribe [:mainnet?]) mainnet? @(re-frame/subscribe [:mainnet?])
@ -151,6 +146,10 @@
public? @(re-frame/subscribe [:current-chat/public?]) public? @(re-frame/subscribe [:current-chat/public?])
reply @(re-frame/subscribe [:chats/reply-message]) reply @(re-frame/subscribe [:chats/reply-message])
sending-image @(re-frame/subscribe [:chats/sending-image]) sending-image @(re-frame/subscribe [:chats/sending-image])
input-focus (fn []
(some-> ^js (react/current-ref text-input-ref) .focus))
clear-input (fn []
(some-> ^js (react/current-ref text-input-ref) .clear))
empty-text (string/blank? (string/trim (or input-text ""))) empty-text (string/blank? (string/trim (or input-text "")))
show-send (and (or (not empty-text) show-send (and (or (not empty-text)
sending-image) sending-image)

View File

@ -12,10 +12,6 @@
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im.ui.components.slider :as slider])) [status-im.ui.components.slider :as slider]))
(defn message-press-handlers [_]
;;TBI save audio file?
)
(defonce player-ref (atom nil)) (defonce player-ref (atom nil))
(defonce current-player-message-id (atom nil)) (defonce current-player-message-id (atom nil))
(defonce current-active-state-ref-ref (atom nil)) (defonce current-active-state-ref-ref (atom nil))

View File

@ -2,10 +2,12 @@
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[quo.platform :as platform]
[status-im.ui.components.colors :as colors] [status-im.ui.components.colors :as colors]
[status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im.ui.screens.chat.message.audio :as message.audio] [status-im.ui.screens.chat.message.audio :as message.audio]
[status-im.chat.models.reactions :as models.reactions]
[status-im.ui.screens.chat.message.command :as message.command] [status-im.ui.screens.chat.message.command :as message.command]
[status-im.ui.screens.chat.photos :as photos] [status-im.ui.screens.chat.photos :as photos]
[status-im.ui.screens.chat.sheets :as sheets] [status-im.ui.screens.chat.sheets :as sheets]
@ -13,6 +15,7 @@
[status-im.ui.screens.chat.utils :as chat.utils] [status-im.ui.screens.chat.utils :as chat.utils]
[status-im.utils.contenthash :as contenthash] [status-im.utils.contenthash :as contenthash]
[status-im.utils.security :as security] [status-im.utils.security :as security]
[status-im.ui.screens.chat.message.reactions :as reactions]
[reagent.core :as reagent]) [reagent.core :as reagent])
(:require-macros [status-im.utils.views :refer [defview letsubs]])) (:require-macros [status-im.utils.views :refer [defview letsubs]]))
@ -29,13 +32,6 @@
outgoing outgoing
(:rtl? content))} timestamp-str])) (:rtl? content))} timestamp-str]))
(defn message-bubble-wrapper
[message message-content appender]
[react/view
(style/message-view message)
message-content
appender])
(defview quoted-message (defview quoted-message
[_ {:keys [from text image]} outgoing current-public-key public?] [_ {:keys [from text image]} outgoing current-public-key public?]
(letsubs [contact-name [:contacts/contact-name-by-identity from]] (letsubs [contact-name [:contacts/contact-name-by-identity from]]
@ -106,14 +102,6 @@
(conj acc literal))) (conj acc literal)))
(defview message-content-status [{:keys [content content-type]}]
[react/view style/status-container
[react/text {:style (style/status-text)}
(reduce
(fn [acc e] (render-inline (:text content) false content-type acc e))
[react/text-class {:style (style/status-text)}]
(-> content :parsed-text peek :children))]])
(defn render-block [{:keys [content outgoing content-type]} acc (defn render-block [{:keys [content outgoing content-type]} acc
{:keys [type ^js literal children]}] {:keys [type ^js literal children]}]
(case type (case type
@ -153,51 +141,15 @@
;; Append timestamp to new block ;; Append timestamp to new block
(conj elements timestamp)))) (conj elements timestamp))))
(defn text-message-press-handlers [message]
{:on-press (fn [_]
(react/dismiss-keyboard!))
:on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet
{:content (sheets/message-long-press message)
:height 192}])})
(defn text-message
[{:keys [content outgoing current-public-key public?] :as message}]
[react/touchable-highlight (text-message-press-handlers message)
[message-bubble-wrapper message
(let [response-to (:response-to content)]
[react/view
(when (and (seq response-to) (:quoted-message message))
[quoted-message response-to (:quoted-message message) outgoing current-public-key public?])
[render-parsed-text-with-timestamp message (:parsed-text content)]])
[message-timestamp message true]]])
(defn unknown-content-type (defn unknown-content-type
[{:keys [outgoing content-type content] :as message}] [{:keys [outgoing content-type content] :as message}]
[message-bubble-wrapper message [react/view (style/message-view message)
[react/text [react/text
{:style {:color (if outgoing colors/white-persist colors/black)}} {:style {:color (if outgoing colors/white-persist colors/black)}}
(if (seq (:text content)) (if (seq (:text content))
(:text content) (:text content)
(str "Unhandled content-type " content-type))]]) (str "Unhandled content-type " content-type))]])
(defn system-text-message
[{:keys [content] :as message}]
[message-bubble-wrapper message
[react/view
[render-parsed-text message (:parsed-text content)]]])
(defn emoji-message
[{:keys [content current-public-key outgoing public?] :as message}]
(let [response-to (:response-to content)]
[react/touchable-highlight (text-message-press-handlers message)
[message-bubble-wrapper message
[react/view {:style (style/style-message-text outgoing)}
(when (and (seq response-to) (:quoted-message message))
[quoted-message response-to (:quoted-message message) outgoing current-public-key public?])
[react/text {:style (style/emoji-message message)}
(:text content)]]
[message-timestamp message]]]))
(defn message-not-sent-text (defn message-not-sent-text
[chat-id message-id] [chat-id message-id]
[react/touchable-highlight [react/touchable-highlight
@ -220,43 +172,39 @@
(= outgoing-status :not-sent)) (= outgoing-status :not-sent))
[message-not-sent-text chat-id message-id])) [message-not-sent-text chat-id message-id]))
(defview message-author-name [from alias] (defview message-author-name [from alias modal]
(letsubs [contact-name [:contacts/raw-contact-name-by-identity from]] (letsubs [contact-name [:contacts/raw-contact-name-by-identity from]]
(chat.utils/format-author (or contact-name alias) style/message-author-name-container))) (chat.utils/format-author (or contact-name alias) modal)))
(defn message-content-wrapper (defn message-content-wrapper
"Author, userpic and delivery wrapper" "Author, userpic and delivery wrapper"
[{:keys [alias first-in-group? display-photo? identicon display-username? [{:keys [alias first-in-group? display-photo? identicon display-username?
from outgoing] from outgoing]
:as message} content] :as message} content {:keys [modal close-modal]}]
[react/view {:style (style/message-wrapper message) [react/view {:style (style/message-wrapper message)
:pointer-events :box-none
:accessibility-label :chat-item} :accessibility-label :chat-item}
[react/view (style/message-body message) [react/view {:style (style/message-body message)
:pointer-events :box-none}
(when display-photo? (when display-photo?
; userpic
[react/view (style/message-author-userpic outgoing) [react/view (style/message-author-userpic outgoing)
(when first-in-group? (when first-in-group?
[react/touchable-highlight {:on-press #(re-frame/dispatch [:chat.ui/show-profile from])} [react/touchable-highlight {:on-press #(do (when modal (close-modal))
(re-frame/dispatch [:chat.ui/show-profile from]))}
[photos/member-identicon identicon]])]) [photos/member-identicon identicon]])])
; username [react/view {:style (style/message-author-wrapper outgoing display-photo?)}
[react/view (style/message-author-wrapper outgoing display-photo?)
(when display-username? (when display-username?
[react/touchable-opacity {:style style/message-author-touchable [react/touchable-opacity {:style style/message-author-touchable
:on-press #(re-frame/dispatch [:chat.ui/show-profile from])} :on-press #(do (when modal (close-modal))
[message-author-name from alias]]) (re-frame/dispatch [:chat.ui/show-profile from]))}
[message-author-name from alias modal]])
;;MESSAGE CONTENT ;;MESSAGE CONTENT
content]] [react/view
content]]]
; delivery status ; delivery status
[react/view (style/delivery-status outgoing) [react/view (style/delivery-status outgoing)
[message-delivery-status message]]]) [message-delivery-status message]]])
(defn system-message-content-wrapper
[message child]
[react/view {:style (style/message-wrapper-base message)
:accessibility-label :chat-item}
[react/view (style/system-message-body message)
[react/view child]]])
(defn message-content-image [{:keys [content outgoing]}] (defn message-content-image [{:keys [content outgoing]}]
(let [dimensions (reagent/atom [260 260]) (let [dimensions (reagent/atom [260 260])
uri (:image content)] uri (:image content)]
@ -271,57 +219,146 @@
:resize-mode :contain :resize-mode :contain
:source {:uri uri}}]]))) :source {:uri uri}}]])))
(defn image-message-press-handlers [{:keys [content] :as message}] (defmulti ->message :content-type)
{:on-press (fn [_]
(when (:image content)
(re-frame/dispatch [:navigate-to :image-preview message]))
(react/dismiss-keyboard!))
:on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet
{:content (sheets/image-long-press message false)
:height 160}])})
(defn sticker-message-press-handlers [{:keys [content] :as message}] (defmethod ->message constants/content-type-command
(let [pack (get-in content [:sticker :pack])]
{:on-press (fn [_]
(when pack
(re-frame/dispatch [:stickers/open-sticker-pack pack]))
(react/dismiss-keyboard!))
:on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet
{:content (sheets/sticker-long-press message)
:height 64}])}))
(defn message-content-audio
[message] [message]
[react/touchable-highlight (message.audio/message-press-handlers message) [message.command/command-content message-content-wrapper message])
[message-bubble-wrapper message
[message.audio/message-content message [message-timestamp message false]]]])
(defn chat-message [{:keys [public? content content-type] :as message}] (defmethod ->message constants/content-type-system-text [{:keys [content] :as message}]
(if (= content-type constants/content-type-command) [react/view {:accessibility-label :chat-item}
[message.command/command-content message-content-wrapper message] [react/view (style/system-message-body message)
(if (= content-type constants/content-type-system-text) [react/view (style/message-view message)
[system-message-content-wrapper message [system-text-message message]] [react/view
[message-content-wrapper [render-parsed-text message (:parsed-text content)]]]]])
message
(if (= content-type constants/content-type-text) (defmethod ->message constants/content-type-text
;; text message [{:keys [content outgoing current-public-key public?] :as message} {:keys [on-long-press modal]
[text-message message] :as reaction-picker}]
(if (= content-type constants/content-type-status) [message-content-wrapper message
[message-content-status message] [react/touchable-highlight (when-not modal
(if (= content-type constants/content-type-emoji) {:on-press (fn [_]
[emoji-message message] (react/dismiss-keyboard!))
(if (= content-type constants/content-type-sticker) :on-long-press (fn []
[react/touchable-highlight (sticker-message-press-handlers message) (on-long-press
[react/image {:style {:margin 10 :width 140 :height 140} [{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message])
;;TODO (perf) move to event :label (i18n/label :t/message-reply)}
:source {:uri (contenthash/url (-> content :sticker :hash))}}]] {:on-press #(react/copy-to-clipboard (get content :text))
(if (and (= content-type constants/content-type-image) :label (i18n/label :t/sharing-copy-to-clipboard)}]))})
;; Disabling images for public-chats [react/view (style/message-view message)
(not public?)) (let [response-to (:response-to content)]
[react/touchable-highlight (image-message-press-handlers message) [react/view
[message-content-image message]] (when (and (seq response-to) (:quoted-message message))
(if (and (= content-type constants/content-type-audio) [quoted-message response-to (:quoted-message message) outgoing current-public-key public?])
;; Disabling audio for public-chats [render-parsed-text-with-timestamp message (:parsed-text content)]])
(not public?)) [message-timestamp message true]]]
[message-content-audio message] reaction-picker])
[unknown-content-type message]))))))])))
(defmethod ->message constants/content-type-status
[{:keys [content content-type] :as message}]
[message-content-wrapper message
[react/view style/status-container
[react/text {:style (style/status-text)}
(reduce
(fn [acc e] (render-inline (:text content) false content-type acc e))
[react/text-class {:style (style/status-text)}]
(-> content :parsed-text peek :children))]]])
(defmethod ->message constants/content-type-emoji
[{:keys [content current-public-key outgoing public?] :as message} {:keys [on-long-press modal]
:as reaction-picker}]
(let [response-to (:response-to content)]
[message-content-wrapper message
[react/touchable-highlight (when-not modal
{:on-press (fn []
(react/dismiss-keyboard!))
:on-long-press (fn []
(on-long-press
[{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message])
:label (i18n/label :t/message-reply)}
{:on-press #(react/copy-to-clipboard (get content :text))
:label (i18n/label :t/sharing-copy-to-clipboard)}]))})
[react/view (style/message-view message)
[react/view {:style (style/style-message-text outgoing)}
(when (and (seq response-to) (:quoted-message message))
[quoted-message response-to (:quoted-message message) outgoing current-public-key public?])
[react/text {:style (style/emoji-message message)}
(:text content)]]
[message-timestamp message]]]
reaction-picker]))
(defmethod ->message constants/content-type-sticker
[{:keys [content from outgoing]
:as message}
{:keys [on-long-press modal]
:as reaction-picker}]
(let [pack (get-in content [:sticker :pack])]
[message-content-wrapper message
[react/touchable-highlight (when-not modal
{:on-press (fn [_]
(when pack
(re-frame/dispatch [:stickers/open-sticker-pack pack]))
(react/dismiss-keyboard!))
:on-long-press (fn []
(on-long-press
(when-not outgoing
[{:on-press #(when pack
(re-frame/dispatch [:chat.ui/show-profile from]))
:label (i18n/label :t/view-details)}])))})
[react/image {:style {:margin 10 :width 140 :height 140}
;;TODO (perf) move to event
:source {:uri (contenthash/url (-> content :sticker :hash))}}]]
reaction-picker]))
(defmethod ->message constants/content-type-image [{:keys [content] :as message} {:keys [on-long-press modal]
:as reaction-picker}]
[message-content-wrapper message
[react/touchable-highlight (when-not modal
{:on-press (fn [_]
(when (:image content)
(re-frame/dispatch [:navigate-to :image-preview message]))
(react/dismiss-keyboard!))
:on-long-press (fn []
(on-long-press
[{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message])
:label (i18n/label :t/message-reply)}
{:on-press #(re-frame/dispatch [:chat.ui/save-image-to-gallery (:image content)])
:label (i18n/label :t/save)}]))})
[message-content-image message]]
reaction-picker])
(defmethod ->message constants/content-type-audio [message {:keys [on-long-press modal]
:as reaction-picker}]
[message-content-wrapper message
[react/touchable-highlight (when-not modal
{:on-long-press
(fn [] (on-long-press []))})
[react/view (style/message-view message)
[message.audio/message-content message [message-timestamp message false]]]]
reaction-picker])
(defmethod ->message :default [message]
[message-content-wrapper message
[unknown-content-type message]])
(defn chat-message [message set-active-panel]
[reactions/with-reaction-picker
{:message message
:reactions @(re-frame/subscribe [:chats/message-reactions (:message-id message)])
:picker-on-open (fn []
;; NOTE(Ferossgp): Because of soft-input adjustResize there are some problems on android
(when (and platform/ios? (pos? @(re-frame/subscribe [:keyboard-height])))
(set-active-panel :keep-space)))
:picker-on-close (fn []
(when platform/ios?
(set-active-panel nil)))
:send-emoji (fn [{:keys [emoji-id]}]
(re-frame/dispatch [::models.reactions/send-emoji-reaction
{:message-id (:message-id message)
:emoji-id emoji-id}]))
:retract-emoji (fn [{:keys [emoji-id emoji-reaction-id]}]
(re-frame/dispatch [::models.reactions/send-emoji-reaction-retraction
{:message-id (:message-id message)
:emoji-id emoji-id
:emoji-reaction-id emoji-reaction-id}]))
:render ->message}])

View File

@ -0,0 +1,94 @@
(ns status-im.ui.screens.chat.message.reactions
(:require [status-im.ui.screens.chat.message.reactions-picker :as reaction-picker]
[status-im.ui.screens.chat.message.reactions-row :as reaction-row]
[reagent.core :as reagent]
[quo.react-native :as rn]
[quo.react :as react]
[quo.animated :as animated]))
(defn measure-in-window [ref cb]
(.measureInWindow ^js ref cb))
(defn get-picker-position [^js ref cb]
(some-> ref
react/current-ref
(measure-in-window
(fn [x y width height]
(cb {:top y
:left x
:width width
:height height})))))
(defn- extract-id [reactions id]
(->> reactions
(filter (fn [{:keys [emoji-id]}] (= emoji-id id)))
first
:emoji-reaction-id))
(defn with-reaction-picker []
(let [ref (react/create-ref)
animated-state (animated/value 0)
spring-animation (animated/with-spring-transition
animated-state
(:jump animated/springs))
animation (animated/with-timing-transition
animated-state
{:duration reaction-picker/animation-duration
:easing (:ease-in-out animated/easings)})
visible (reagent/atom false)
actions (reagent/atom nil)
position (reagent/atom {})]
(fn [{:keys [message reactions outgoing outgoing-status render send-emoji retract-emoji picker-on-open picker-on-close]}]
(let [own-reactions (reduce (fn [acc {:keys [emoji-id own]}]
(if own (conj acc emoji-id) acc))
[] reactions)
on-emoji-press (fn [emoji-id]
(let [active ((set own-reactions) emoji-id)]
(if active
(retract-emoji {:emoji-id emoji-id
:emoji-reaction-id (extract-id reactions emoji-id)})
(send-emoji {:emoji-id emoji-id}))))
on-close (fn []
(animated/set-value animated-state 0)
(js/setTimeout
(fn []
(reset! actions nil)
(reset! visible false)
(picker-on-close))
reaction-picker/animation-duration))
on-open (fn [pos]
(picker-on-open)
(reset! position pos)
(reset! visible true))]
[:<>
[animated/view {:style {:opacity (animated/mix animation 1 0)}}
[rn/view {:ref ref
:collapsable false}
[render message {:modal false
:on-long-press (fn [act]
(when (or (not outgoing)
(and outgoing (= outgoing-status :sent)))
(reset! actions act)
(get-picker-position ref on-open)))}]]
[reaction-row/message-reactions message reactions]]
[rn/modal {:visible @visible
:on-request-close on-close
:on-show (fn []
(js/requestAnimationFrame
#(animated/set-value animated-state 1)))
:transparent true}
[reaction-picker/modal {:outgoing (:outgoing message)
:display-photo (:display-photo? message)
:animation animation
:spring spring-animation
:top (:top @position)
:message-height (:height @position)
:on-close on-close
:actions @actions
:own-reactions own-reactions
:send-emoji (fn [emoji]
(on-close)
(js/setTimeout #(on-emoji-press emoji)
reaction-picker/animation-duration))}
[render message {:modal true
:close-modal on-close}]]]]))))

View File

@ -0,0 +1,105 @@
(ns status-im.ui.screens.chat.message.reactions-picker
(:require [cljs-bean.core :as bean]
[status-im.ui.screens.chat.message.styles :as styles]
[status-im.constants :as constants]
[reagent.core :as reagent]
[quo.react-native :as rn]
[quo.react :as react]
[quo.animated :as animated]
[quo.components.safe-area :as safe-area]
[quo.core :as quo]))
(def tabbar-height 36)
(def text-input-height 54)
(def animation-duration 150)
(def scale 0.8)
(def translate-x 27)
(def translate-y -24)
(defn picker [{:keys [outgoing actions own-reactions on-close send-emoji]}]
[rn/view {:style (styles/container-style {:outgoing outgoing})}
[rn/view {:style (styles/reactions-picker-row)}
(doall
(for [[id resource] constants/reactions
:let [active (own-reactions id)]]
^{:key id}
[rn/touchable-opacity {:on-press #(send-emoji id)}
[rn/view {:style (styles/reaction-button active)}
[rn/image {:source resource
:style {:height 32
:width 32}}]]]))]
(when (seq actions)
[rn/view {:style (styles/quick-actions-row)}
(for [action actions
:let [{:keys [label on-press]} (bean/bean action)]]
^{:key label}
[rn/touchable-opacity {:on-press (fn []
(on-close)
(js/setTimeout on-press animation-duration))}
[quo/button {:type :secondary}
label]])])])
(def modal
(reagent/adapt-react-class
(fn [props]
(let [{outgoing :outgoing
animation :animation
spring :spring
top :top
message-height :messageHeight
display-photo :displayPhoto
on-close :onClose
actions :actions
send-emoji :sendEmoji
own-reactions :ownReactions
children :children}
(bean/bean props)
{bottom-inset :bottom} (safe-area/use-safe-area)
{window-height :height} (rn/use-window-dimensions)
{picker-height :height
on-picker-layout :on-layout} (rn/use-layout)
full-height (+ message-height picker-height top)
max-height (- window-height bottom-inset tabbar-height text-input-height)
top-delta (max 0 (- full-height max-height))
translation-x (if outgoing
translate-x
(* -1 translate-x))]
(reagent/as-element
[:<>
[rn/view {:style {:position :absolute
:flex 1
:top 0
:bottom 0
:left 0
:right 0}}
[rn/touchable-without-feedback
{:on-press on-close}
[animated/view {:style {:flex 1
:opacity animation
:background-color "rgba(0,0,0,0.5)"}}]]]
[animated/view {:pointer-events :box-none
:style {:top (- top top-delta)
:left 0
:right 0
:position :absolute
:opacity animation
:transform [{:translateY (animated/mix animation top-delta 0)}]}}
(into [:<>] (react/get-children children))
[animated/view {:on-layout on-picker-layout
:pointer-events :box-none
:style (merge (styles/picker-wrapper-style {:display-photo? display-photo
:outgoing outgoing})
{:opacity animation
:transform [{:translateX (animated/mix spring translation-x 0)}
{:translateY (animated/mix spring translate-y 0)}
{:scale (animated/mix spring scale 1)}]})}
[picker {:outgoing outgoing
:actions actions
:on-close on-close
:own-reactions (into #{} (js->clj own-reactions))
:send-emoji send-emoji
:animation animation}]]]])))))

View File

@ -0,0 +1,22 @@
(ns status-im.ui.screens.chat.message.reactions-row
(:require [status-im.constants :as constants]
[status-im.ui.screens.chat.message.styles :as styles]
[quo.react-native :as rn]
[quo.core :as quo]))
(defn reaction [{:keys [outgoing]} {:keys [own emoji-id quantity]}]
[rn/view {:style (styles/reaction-style {:outgoing outgoing
:own own})}
[rn/image {:source (get constants/reactions emoji-id)
:style {:width 16
:height 16
:margin-right 4}}]
[quo/text {:style (styles/reaction-quantity-style {:own own})}
quantity]])
(defn message-reactions [message reactions]
(when (seq reactions)
[rn/view {:style (styles/reactions-row message)}
(for [emoji-reaction reactions]
^{:key (str emoji-reaction)}
[reaction message emoji-reaction])]))

View File

@ -0,0 +1,86 @@
(ns status-im.ui.screens.chat.message.styles
(:require [quo.design-system.colors :as colors]
[status-im.ui.screens.chat.styles.photos :as photos]))
(defn picker-wrapper-style [{:keys [display-photo? outgoing]}]
(merge {:flex-direction :row
:flex 1
:padding-top 4
:padding-right 8}
(if outgoing
{:justify-content :flex-end}
{:justify-content :flex-start})
(if display-photo?
{:padding-left (+ 16 photos/default-size)}
{:padding-left 8})))
(defn container-style [{:keys [outgoing]}]
(merge {:border-top-left-radius 16
:border-top-right-radius 16
:border-bottom-right-radius 16
:border-bottom-left-radius 16
:background-color (:ui-background @colors/theme)}
(if outgoing
{:border-top-right-radius 4}
{:border-top-left-radius 4})))
(defn reactions-picker-row []
{:flex-direction :row
:padding-vertical 8
:padding-horizontal 8})
(defn quick-actions-row []
{:flex-direction :row
:justify-content :space-evenly
:border-top-width 1
:border-top-color (:ui-01 @colors/theme)})
(defn reaction-style [{:keys [outgoing own]}]
(merge {:border-top-left-radius 10
:border-top-right-radius 10
:border-bottom-right-radius 10
:border-bottom-left-radius 10
:flex-direction :row
:margin-vertical 2
:padding-right 8
:padding-left 2
:padding-vertical 2}
(if own
{:background-color (:interactive-01 @colors/theme)}
{:background-color (:interactive-02 @colors/theme)})
(if outgoing
{:border-top-right-radius 2
:margin-left 4}
{:border-top-left-radius 2
:margin-right 4})))
(defn reaction-quantity-style [{:keys [own]}]
{:font-size 12
:line-height 16
:color (if own
colors/white
(:text-01 @colors/theme))})
(defn reactions-row [{:keys [outgoing display-photo?]}]
(merge {:flex-direction :row
:padding-right 8}
(if outgoing
{:justify-content :flex-end}
{:justify-content :flex-start})
(if display-photo?
{:padding-left (+ 16 photos/default-size)}
{:padding-left 8})))
(defn reaction-button [active]
(merge {:width 40
:height 40
:border-radius 20
:justify-content :center
:align-items :center
:margin-horizontal 1
:border-width 1
:border-color :transparent}
(when active
{:background-color (:interactive-02 @colors/theme)
;; FIXME: Use broder color here
:border-color "rgba(67, 96, 223, 0.2)"})))

View File

@ -152,57 +152,6 @@
:accessibility-label :delete-transaccent-button :accessibility-label :delete-transaccent-button
:on-press #(hide-sheet-and-dispatch [:chat.ui/delete-message chat-id message-id])}]])) :on-press #(hide-sheet-and-dispatch [:chat.ui/delete-message chat-id message-id])}]]))
(defn message-long-press [{:keys [content from outgoing] :as message}]
(fn []
(let [photo @(re-frame/subscribe [:chats/photo-path from])
contact-name @(re-frame/subscribe [:contacts/contact-name-by-identity from])]
[react/view
(when-not outgoing
[quo/list-item
{:theme :accent
:icon [chat-icon/contact-icon-contacts-tab photo]
:title contact-name
:subtitle (i18n/label :t/view-profile)
:accessibility-label :view-chat-details-button
:chevron true
:on-press #(hide-sheet-and-dispatch [:chat.ui/show-profile from])}])
[quo/list-item
{:theme :accent
:title (i18n/label :t/message-reply)
:icon :main-icons/reply
:on-press #(hide-sheet-and-dispatch [:chat.ui/reply-to-message message])}]
[quo/list-item
{:theme :accent
:title (i18n/label :t/sharing-copy-to-clipboard)
:icon :main-icons/copy
:on-press (fn []
(re-frame/dispatch [:bottom-sheet/hide])
(react/copy-to-clipboard (:text content)))}]
[quo/list-item
{:theme :accent
:title (i18n/label :t/sharing-share)
:icon :main-icons/share
:on-press (fn []
(re-frame/dispatch [:bottom-sheet/hide])
;; https://github.com/facebook/react-native/pull/26839
(js/setTimeout
#(list-selection/open-share {:message (:text content)})
250))}]])))
(defn sticker-long-press [{:keys [from]}]
(fn []
(let [photo @(re-frame/subscribe [:chats/photo-path from])
contact-name @(re-frame/subscribe [:contacts/contact-name-by-identity from])]
[react/view
[quo/list-item
{:theme :accent
:icon [chat-icon/contact-icon-contacts-tab photo]
:title contact-name
:subtitle (i18n/label :t/view-profile)
:accessibility-label :view-chat-details-button
:chevron true
:on-press #(hide-sheet-and-dispatch [:chat.ui/show-profile from])}]])))
(defn image-long-press [{:keys [content identicon from outgoing] :as message} from-preview?] (defn image-long-press [{:keys [content identicon from outgoing] :as message} from-preview?]
(fn [] (fn []
(let [contact-name @(re-frame/subscribe [:contacts/contact-name-by-identity from])] (let [contact-name @(re-frame/subscribe [:contacts/contact-name-by-identity from])]

View File

@ -10,11 +10,6 @@
[outgoing] [outgoing]
{:color (if outgoing colors/white-persist colors/text)}) {:color (if outgoing colors/white-persist colors/text)})
(defn last-message-padding
[{:keys [first? typing]}]
(when (and first? (not typing))
{:padding-bottom 16}))
(defn system-message-body (defn system-message-body
[_] [_]
{:margin-top 4 {:margin-top 4
@ -54,12 +49,8 @@
:bottom 9 ; 6 Bubble bottom, 3 message baseline :bottom 9 ; 6 Bubble bottom, 3 message baseline
(if rtl? :left :right) 12}))) (if rtl? :left :right) 12})))
(defn message-wrapper-base [message] (defn message-wrapper [{:keys [outgoing]}]
(merge {:flex-direction :column} (merge {:flex-direction :column}
(last-message-padding message)))
(defn message-wrapper [{:keys [outgoing] :as message}]
(merge (message-wrapper-base message)
(if outgoing (if outgoing
{:margin-left 96} {:margin-left 96}
{:margin-right 52}))) {:margin-right 52})))
@ -83,8 +74,8 @@
:padding-left 8})) :padding-left 8}))
(def message-author-touchable (def message-author-touchable
{:margin-left 12 {:margin-left 12
:padding-vertical 2}) :flex-direction :row})
(defn message-author-userpic [outgoing] (defn message-author-userpic [outgoing]
(merge (merge
@ -93,7 +84,7 @@
(if outgoing (if outgoing
{:padding-left 8} {:padding-left 8}
{:padding-horizontal 8 {:padding-horizontal 8
:padding-right 8}))) :padding-right 8})))
(def delivery-text (def delivery-text
{:color colors/gray {:color colors/gray
@ -164,13 +155,6 @@
:padding-left 12 :padding-left 12
:text-align-vertical :center}) :text-align-vertical :center})
(def message-author-name-container
{:padding-top 6
:padding-left 12
:padding-right 16
:margin-right 12
:text-align-vertical :center})
(defn quoted-message-container [outgoing] (defn quoted-message-container [outgoing]
{:margin-bottom 6 {:margin-bottom 6
:padding-bottom 6 :padding-bottom 6

View File

@ -18,6 +18,7 @@
:border-radius (radius size)}) :border-radius (radius size)})
(defn photo [size] (defn photo [size]
{:border-radius (radius size) {:border-radius (radius size)
:width size :width size
:height size}) :height size
:background-color colors/white})

View File

@ -6,26 +6,40 @@
(def ^:private reply-symbol "↪ ") (def ^:private reply-symbol "↪ ")
(defn format-author [contact-name style] (defn format-author
(let [additional-styles (style false)] ([contact-name] (format-author contact-name false))
(if (or (= (aget contact-name 0) "@") ([contact-name modal]
;; in case of replies (if (= (aget contact-name 0) "@")
(= (aget contact-name 1) "@")) (let [trimmed-name (subs contact-name 0 81)]
(let [trimmed-name (subs contact-name 0 81)] [react/text {:number-of-lines 2
[react/text {:number-of-lines 2 :style {:color (if modal colors/white-persist colors/blue)
:style (merge {:color colors/blue :font-size 13
:font-size 13 :line-height 18
:line-height 18 :font-weight "500"}}
:font-weight "500"} additional-styles)} (or (stateofus/username trimmed-name) trimmed-name)])
(or (stateofus/username trimmed-name) trimmed-name)]) [react/text {:style {:color (if modal colors/white-persist colors/gray)
[react/text {:style (merge {:color colors/gray :font-size 12
:font-size 12 :line-height 18
:line-height 18 :font-weight "400"}}
:font-weight "400"} additional-styles)} contact-name])))
contact-name])))
(defn format-reply-author [from username current-public-key style] (defn format-reply-author [from username current-public-key style]
(or (and (= from current-public-key) (let [contact-name (str reply-symbol username)]
[react/text {:style (style true)} (or (and (= from current-public-key)
(str reply-symbol (i18n/label :t/You))]) [react/text {:style (style true)}
(format-author (str reply-symbol username) style))) (str reply-symbol (i18n/label :t/You))])
(if (or (= (aget contact-name 0) "@")
;; in case of replies
(= (aget contact-name 1) "@"))
(let [trimmed-name (subs contact-name 0 81)]
[react/text {:number-of-lines 2
:style (merge {:color colors/blue
:font-size 13
:line-height 18
:font-weight "500"})}
(or (stateofus/username trimmed-name) trimmed-name)])
[react/text {:style (merge {:color colors/gray
:font-size 12
:line-height 18
:font-weight "400"})}
contact-name]))))

View File

@ -12,6 +12,7 @@
[quo.animated :as animated] [quo.animated :as animated]
[quo.react-native :as rn] [quo.react-native :as rn]
[status-im.ui.screens.chat.audio-message.views :as audio-message] [status-im.ui.screens.chat.audio-message.views :as audio-message]
[quo.react :as quo.react]
[status-im.ui.screens.chat.message.message :as message] [status-im.ui.screens.chat.message.message :as message]
[status-im.ui.screens.chat.stickers.views :as stickers] [status-im.ui.screens.chat.stickers.views :as stickers]
[status-im.ui.screens.chat.styles.main :as style] [status-im.ui.screens.chat.styles.main :as style]
@ -136,7 +137,7 @@
(debounce/debounce-and-dispatch [:chat.ui/message-visibility-changed e] 5000)) (debounce/debounce-and-dispatch [:chat.ui/message-visibility-changed e] 5000))
(defview messages-view (defview messages-view
[{:keys [group-chat chat-id public?] :as chat} bottom-space pan-handler] [{:keys [group-chat chat-id public?] :as chat} bottom-space pan-handler set-active-panel]
(letsubs [messages [:chats/current-chat-messages-stream] (letsubs [messages [:chats/current-chat-messages-stream]
no-messages? [:chats/current-chat-no-messages?] no-messages? [:chats/current-chat-no-messages?]
current-public-key [:multiaccount/public-key]] current-public-key [:multiaccount/public-key]]
@ -161,11 +162,13 @@
:incoming-group (and group-chat (not outgoing)) :incoming-group (and group-chat (not outgoing))
:group-chat group-chat :group-chat group-chat
:public? public? :public? public?
:current-public-key current-public-key)]))) :current-public-key current-public-key)
set-active-panel])))
:on-viewable-items-changed on-viewable-items-changed :on-viewable-items-changed on-viewable-items-changed
:on-end-reached #(re-frame/dispatch [:chat.ui/load-more-messages]) :on-end-reached #(re-frame/dispatch [:chat.ui/load-more-messages])
:on-scroll-to-index-failed #() ;;don't remove this :on-scroll-to-index-failed #() ;;don't remove this
:content-container-style {:padding-top @bottom-space} :content-container-style {:padding-top (+ @bottom-space 16)
:padding-bottom 16}
:scrollIndicatorInsets {:top @bottom-space} :scrollIndicatorInsets {:top @bottom-space}
:keyboardDismissMode "interactive" :keyboardDismissMode "interactive"
:keyboard-should-persist-taps :handled})])) :keyboard-should-persist-taps :handled})]))
@ -187,12 +190,16 @@
active-panel (reagent/atom nil) active-panel (reagent/atom nil)
position-y (animated/value 0) position-y (animated/value 0)
pan-state (animated/value 0) pan-state (animated/value 0)
text-input-ref (quo.react/create-ref)
on-update (partial reset! bottom-space) on-update (partial reset! bottom-space)
pan-responder (accessory/create-pan-responder position-y pan-state) pan-responder (accessory/create-pan-responder position-y pan-state)
set-active-panel (fn [panel] set-active-panel (fn [panel]
(reset! active-panel panel)
(rn/configure-next (rn/configure-next
(:ease-opacity-200 rn/custom-animations)) (:ease-opacity-200 rn/custom-animations))
(when (and (not panel)
(= :keep-space @active-panel))
(some-> ^js (quo.react/current-ref text-input-ref) .focus))
(reset! active-panel panel)
(when panel (when panel
(js/setTimeout #(react/dismiss-keyboard!) 100)))] (js/setTimeout #(react/dismiss-keyboard!) 100)))]
(fn [] (fn []
@ -204,7 +211,7 @@
[react/view {:style {:flex 1}} [react/view {:style {:flex 1}}
(when-not group-chat (when-not group-chat
[add-contact-bar chat-id]) [add-contact-bar chat-id])
[messages-view current-chat bottom-space pan-responder]]] [messages-view current-chat bottom-space pan-responder set-active-panel]]]
(when show-input? (when show-input?
[accessory/view {:y position-y [accessory/view {:y position-y
:pan-state pan-state :pan-state pan-state
@ -212,5 +219,6 @@
:on-close #(set-active-panel nil) :on-close #(set-active-panel nil)
:on-update-inset on-update} :on-update-inset on-update}
[components/chat-toolbar {:active-panel @active-panel [components/chat-toolbar {:active-panel @active-panel
:set-active-panel set-active-panel}] :set-active-panel set-active-panel
:text-input-ref text-input-ref}]
[bottom-sheet @active-panel]])])))) [bottom-sheet @active-panel]])]))))

View File

@ -18,7 +18,6 @@
[status-im.ui.screens.chat.utils :as chat.utils] [status-im.ui.screens.chat.utils :as chat.utils]
[status-im.ui.components.toolbar :as toolbar] [status-im.ui.components.toolbar :as toolbar]
[status-im.ui.screens.chat.message.message :as message] [status-im.ui.screens.chat.message.message :as message]
[status-im.ui.screens.chat.styles.message.message :as message.style]
[status-im.ui.screens.chat.photos :as photos] [status-im.ui.screens.chat.photos :as photos]
[status-im.ui.screens.profile.components.views :as profile.components] [status-im.ui.screens.profile.components.views :as profile.components]
[status-im.utils.debounce :as debounce] [status-im.utils.debounce :as debounce]
@ -629,7 +628,7 @@
(views/defview my-name [] (views/defview my-name []
(views/letsubs [contact-name [:multiaccount/preferred-name]] (views/letsubs [contact-name [:multiaccount/preferred-name]]
(when-not (string/blank? contact-name) (when-not (string/blank? contact-name)
(chat.utils/format-author (str "@" contact-name) message.style/message-author-name-container)))) (chat.utils/format-author (str "@" contact-name)))))
(views/defview registered [names {:keys [preferred-name] :as account} _ registrations] (views/defview registered [names {:keys [preferred-name] :as account} _ registrations]
[react/view {:style {:flex 1}} [react/view {:style {:flex 1}}
@ -674,9 +673,9 @@
[react/view {:padding-left 72} [react/view {:padding-left 72}
[my-name]] [my-name]]
[react/view {:flex-direction :row} [react/view {:flex-direction :row}
[react/view {:padding-left 16 :padding-right 8 :padding-top 4} [react/view {:padding-left 16 :padding-top 4}
[photos/photo (multiaccounts/displayed-photo account) {:size 36}]] [photos/photo (multiaccounts/displayed-photo account) {:size 36}]]
[message/text-message message]]])]]) [message/->message message {:on-long-press identity}]]])]])
(views/defview main [] (views/defview main []
(views/letsubs [{:keys [names multiaccount show? registrations]} [:ens.main/screen]] (views/letsubs [{:keys [names multiaccount show? registrations]} [:ens.main/screen]]

View File

@ -316,7 +316,7 @@
(defn error-item [] (defn error-item []
(fn [title show-error] (fn [title show-error]
[gh/touchable-opacity {:on-press #(swap! show-error not)} [gh/touchable-highlight {:on-press #(swap! show-error not)}
[react/view {:style {:align-items :center [react/view {:style {:align-items :center
:flex-direction :row}} :flex-direction :row}}
[react/text {:style {:color colors/red :margin-right 8}} [react/text {:style {:color colors/red :margin-right 8}}

View File

@ -0,0 +1,10 @@
(ns status-im.utils.profiler)
(defmacro with-measure
[name & body]
`(let [start# (js/performance.now)
res# (do ~@body)
end# (js/performance.now)
time# (.toFixed (- end# start#) 2)]
(taoensso.timbre/info "[perf|" ~name "] => " time#)
res#))

View File

@ -0,0 +1,39 @@
(ns status-im.utils.profiler
"Performance profiling for react components."
(:require-macros [status-im.utils.profiler])
(:require [reagent.core :as reagent]
[taoensso.timbre :as log]
[oops.core :refer [oget ocall]]
[goog.functions :as f]))
(defonce memo (atom {}))
(def td (js/require "tdigest"))
(def tdigest (oget td "TDigest"))
(def react (js/require "react"))
(def profiler (reagent/adapt-react-class (oget react "Profiler")))
(defn on-render-factory
[label]
(let [buf (new tdigest)
log (f/debounce
(fn [phase buf]
(log/info "[profile:" label "(" phase ")]: \n"
(ocall buf "summary")))
300)]
(fn [_ phase adur _ _ _ _]
(.push buf adur)
(log phase buf))))
(defn perf [{:keys [label]}]
(let [this (reagent/current-component)
children (reagent/children this)
on-render (if-let [render-fn (get @memo label)]
render-fn
(do
(swap! memo assoc label (on-render-factory label))
(get @memo label)))]
(into [profiler {:id label
:on-render on-render}]
children)))

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.56.1", "version": "v0.56.5",
"commit-sha1": "4574ab4c22ee6b662a7f87c39d0f714998c567dc", "commit-sha1": "ab01a05cd63df0f749862d16d01a90fdd5f694e8",
"src-sha256": "0jd684lv7x93gxgvvhsv4ihxr5sw2rck2divsxfiql5g2c1v4alg" "src-sha256": "004zjm66gykgz4mlrzsmv797x4pdqjm61dr2dcn1dnbdbdhnglz2"
} }

View File

@ -1060,6 +1060,7 @@
"view-etheremon": "View in Etheremon", "view-etheremon": "View in Etheremon",
"view-gitcoin": "View in Gitcoin", "view-gitcoin": "View in Gitcoin",
"view-profile": "View profile", "view-profile": "View profile",
"view-details": "View Details",
"view-signing": "View signing phrase", "view-signing": "View signing phrase",
"view-superrare": "View in SuperRare", "view-superrare": "View in SuperRare",
"waiting-for-wifi": "No Wi-fi, message syncing disabled.", "waiting-for-wifi": "No Wi-fi, message syncing disabled.",

View File

@ -2028,6 +2028,11 @@ bindings@^1.5.0:
dependencies: dependencies:
file-uri-to-path "1.0.0" file-uri-to-path "1.0.0"
bintrees@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524"
integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=
binwrap@^0.2.2: binwrap@^0.2.2:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/binwrap/-/binwrap-0.2.2.tgz#7d1ea74b28332f18dfdc75548aef993041ffafc9" resolved "https://registry.yarnpkg.com/binwrap/-/binwrap-0.2.2.tgz#7d1ea74b28332f18dfdc75548aef993041ffafc9"
@ -7712,6 +7717,13 @@ tar@^4.4.10:
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
yallist "^3.0.3" yallist "^3.0.3"
tdigest@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021"
integrity sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=
dependencies:
bintrees "1.0.1"
temp@0.8.3: temp@0.8.3:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"