From 1c308c2d0128e8f74d08e1fa2e317c57da9aeae3 Mon Sep 17 00:00:00 2001 From: Gheorghe Pinzaru Date: Fri, 3 Jul 2020 18:04:45 +0300 Subject: [PATCH] Emoji reactions Signed-off-by: Andrea Maria Piana --- package.json | 1 + resources/images/reactions/angry.png | Bin 0 -> 1826 bytes resources/images/reactions/angry@2x.png | Bin 0 -> 4955 bytes resources/images/reactions/angry@3x.png | Bin 0 -> 9404 bytes resources/images/reactions/laugh.png | Bin 0 -> 1720 bytes resources/images/reactions/laugh@2x.png | Bin 0 -> 4349 bytes resources/images/reactions/laugh@3x.png | Bin 0 -> 7798 bytes resources/images/reactions/love.png | Bin 0 -> 1288 bytes resources/images/reactions/love@2x.png | Bin 0 -> 3485 bytes resources/images/reactions/love@3x.png | Bin 0 -> 6739 bytes resources/images/reactions/sad.png | Bin 0 -> 1827 bytes resources/images/reactions/sad@2x.png | Bin 0 -> 4835 bytes resources/images/reactions/sad@3x.png | Bin 0 -> 8885 bytes resources/images/reactions/thumbs-down.png | Bin 0 -> 1332 bytes resources/images/reactions/thumbs-down@2x.png | Bin 0 -> 3482 bytes resources/images/reactions/thumbs-down@3x.png | Bin 0 -> 6344 bytes resources/images/reactions/thumbs-up.png | Bin 0 -> 1334 bytes resources/images/reactions/thumbs-up@2x.png | Bin 0 -> 3443 bytes resources/images/reactions/thumbs-up@3x.png | Bin 0 -> 6396 bytes src/quo/animated.cljs | 13 + src/quo/components/controls/view.cljs | 8 +- src/quo/components/list/item.cljs | 2 +- src/quo/components/text_input.cljs | 2 +- src/quo/gesture_handler.cljs | 8 +- src/status_im/chat/models/loading.cljs | 2 + src/status_im/chat/models/reactions.cljs | 95 ++++++ src/status_im/constants.cljs | 15 + src/status_im/core.cljs | 2 +- src/status_im/data_store/reactions.cljs | 32 ++ src/status_im/ethereum/json_rpc.cljs | 6 + src/status_im/events.cljs | 26 +- src/status_im/react_native/resources.cljs | 8 + src/status_im/subs.cljs | 12 + src/status_im/transport/message/core.cljs | 25 +- src/status_im/transport/message/protocol.cljs | 18 ++ .../ui/screens/chat/components/input.cljs | 13 +- .../ui/screens/chat/message/audio.cljs | 4 - .../ui/screens/chat/message/message.cljs | 281 ++++++++++-------- .../ui/screens/chat/message/reactions.cljs | 94 ++++++ .../chat/message/reactions_picker.cljs | 105 +++++++ .../screens/chat/message/reactions_row.cljs | 22 ++ .../ui/screens/chat/message/styles.cljs | 86 ++++++ src/status_im/ui/screens/chat/sheets.cljs | 51 ---- .../screens/chat/styles/message/message.cljs | 26 +- .../ui/screens/chat/styles/photos.cljs | 7 +- src/status_im/ui/screens/chat/utils.cljs | 56 ++-- src/status_im/ui/screens/chat/views.cljs | 20 +- src/status_im/ui/screens/ens/views.cljs | 7 +- src/status_im/ui/screens/signing/views.cljs | 2 +- src/status_im/utils/profiler.clj | 10 + src/status_im/utils/profiler.cljs | 39 +++ status-go-version.json | 6 +- translations/en.json | 1 + yarn.lock | 12 + 54 files changed, 846 insertions(+), 271 deletions(-) create mode 100644 resources/images/reactions/angry.png create mode 100644 resources/images/reactions/angry@2x.png create mode 100644 resources/images/reactions/angry@3x.png create mode 100644 resources/images/reactions/laugh.png create mode 100644 resources/images/reactions/laugh@2x.png create mode 100644 resources/images/reactions/laugh@3x.png create mode 100644 resources/images/reactions/love.png create mode 100644 resources/images/reactions/love@2x.png create mode 100644 resources/images/reactions/love@3x.png create mode 100644 resources/images/reactions/sad.png create mode 100644 resources/images/reactions/sad@2x.png create mode 100644 resources/images/reactions/sad@3x.png create mode 100644 resources/images/reactions/thumbs-down.png create mode 100644 resources/images/reactions/thumbs-down@2x.png create mode 100644 resources/images/reactions/thumbs-down@3x.png create mode 100644 resources/images/reactions/thumbs-up.png create mode 100644 resources/images/reactions/thumbs-up@2x.png create mode 100644 resources/images/reactions/thumbs-up@3x.png create mode 100644 src/status_im/chat/models/reactions.cljs create mode 100644 src/status_im/data_store/reactions.cljs create mode 100644 src/status_im/ui/screens/chat/message/reactions.cljs create mode 100644 src/status_im/ui/screens/chat/message/reactions_picker.cljs create mode 100644 src/status_im/ui/screens/chat/message/reactions_row.cljs create mode 100644 src/status_im/ui/screens/chat/message/styles.cljs create mode 100644 src/status_im/utils/profiler.clj create mode 100644 src/status_im/utils/profiler.cljs diff --git a/package.json b/package.json index cda4775e5b..0baac51248 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "react-native-svg": "^9.8.4", "react-native-touch-id": "^4.4.1", "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" }, "devDependencies": { diff --git a/resources/images/reactions/angry.png b/resources/images/reactions/angry.png new file mode 100644 index 0000000000000000000000000000000000000000..6759feca83d44fb3f25858306ae6e6d74590bfc0 GIT binary patch literal 1826 zcmV+-2i^FIP)K~#7FomWk4 zR8m8MZOhKMDXer?aA`usU`Sn< z){p>hXh}56MjA^9k%UeY35enpXgkxNcaP`ZbMKuuK!rHzoA>73d(ZjKcYcfTl%I*d z&isn>d^ZAgD1=r*Eb!~RtkM(!mJneK5JsVr{=WIx=+x85#V2jJ)z@CgWUvT|$V9&5vQ@ z5de2S-QJqgIK+>ejiuyt)PMkb>P0y^R?G&b-<;6yjQP^m9p+QQNcA)y868^^z^zZ( z3%N{ogyj{yLi=Tv7#Bq73BUv@3kY2-IBg!6Gl0lrAj=8g3Yd=idDr24#>1&kJN)bWZE+z85wana>s0&BW4 z=|u|ftOFl-M=$B7-~7MI_d_Mw=4? zfge4sa@a6Ly@mxW4|L}-Jzw@Q13t==`Rr(?I?BDdo@m4njEU18ZjvN-eoAK^w4`8J z%;hyA?^R1T<2&)|9F(Rs6nV>z=h9SfCj|x&u-*%nK(Xe^pNlZ~+Z?nsM{w}9tf(fx zTQ|YNitbbtsg9bkngGsOxN>KKuNc0)vVePAv#@o23f8Sqc8_v3SR%!REWewcBRq3; z4$L((FFIRRvwKsKBn3NG7*QCdp#$%o>bcB;)(T0+QG)i)bPR)+t8n4^TrjWe`3&rR zwb26+Mp)kXtp&JLnzwts;;t<@*zrOJwr)rfBvZr(Gr$<$AAF~b($$1u?>Oay0$|@) zcY{#>dF$mI9C~B9ExRXvoPjgrmAdt1S%lLE*Fdr_Sof0if@Ebmg{den^h>xT@)~S7 zgj5p%>sChg;wA-WKLiaM$s59Z9rO zU70~R)Y$}sKR1q0(b^0ZpfkS(J7)L9H zKp=+JjMd5_j9vO0Ca>Rz+kebJt}((~Lke2At%cV1wK9#MpB!Re{J}f0=2r$za1qNx z`}5hxxd|p9FI}H7_$cW`-f;V76$iik9j5+~x{zO;h3f1gR4cLkUX_KtA8&yT&o)9R zt)aq^G(=ZP4Zz%NuGubC&%Zms`}-s;DN$!@8Ntfo`lluL~x+@McA;Z!DXHA4j8O)6Wz(|&cOppu+1M2fo4;{9N<87zB0i!{LKxk zyk}nliVUEE6)EdyO{-R?sq4*+u;;@988(Ja-SGX(1*Z620@j=%c7OF~BF6yce|UR< zk@cZ3Nf3IUeYiG$4`yd$Y}>ZRMYJ5J5InN~+IX2Nvl`pl*Fas-0YA(z1g($fc74}Z z2Y^Pp^9yr|M>5*%M&Lj{CI@ayr4B9E$CPwYs6i(g! zio;Ve=^}5Pl%c~&aOq-)JOZE3ZcMdlCg~@8z`4(+rZUuf2EzM^SjR=LypDn3iVHoj zs0A7}lX0*-Om_+sW0NzN!rDV)fmiNFv6`WQ%RG z68Wb1m<)2mGsVZE8Eku;@+BEae(2n;f{u7(2S`*)g|&Z9eS1nYC&GKY`n8Jm0PZ+D z`Csy%2p|9%>deEk#%_M>;LA)<#y!oh(Q-$HNyaf2$NFMatqx<))BSt?FH3WnmI^Nb Q`v3p{07*qoM6N<$f~d`4egFUf literal 0 HcmV?d00001 diff --git a/resources/images/reactions/angry@2x.png b/resources/images/reactions/angry@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5c408b172366d43207a01f7bf311c3325c38a5ee GIT binary patch literal 4955 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 literal 0 HcmV?d00001 diff --git a/resources/images/reactions/angry@3x.png b/resources/images/reactions/angry@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..487e5961d383a125c7ff1d7dc515d68896ec29c0 GIT binary patch literal 9404 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 z zul~o|!? zEPr|Brwl*_!v6K9M$jY6&zN295Dy$45e}yo7&mB&SEPJ{;oRIy!jGoheg89gSF0r3 zsCDmpOi`v)$kS(E+9DlplD-EJZHEE=oH>vZtHE=wHV=WFohPc+$li^9tP^H)uE2@r zDQO=vCSm)P`!-=3G9?bki>VWc56Ee=Z^fvS=LXNx_BzEo#yWcx-gg&V}b z(!YmfvwwZ1JbyX4A^`koUU;xwGQwu0KzMy^vw2XW9@RBnUvP!mVxkdeAP)# zzYSL?R|P;SnKvXGq@{LIpk5n3%pnROMEeWoTJ)c&JeVO&;xs%2@hH+Jv7;_LcI`$ z26NbaaWRfy{d5YO&>lJ6OAmhUAg-}o7XY+m6RRZsA(j+U=6{|wBb#J3`-if%hLfUv r_TT3dQJQ4Krd&EepZ@+yTu1pe5i?-V42%4D00000NkvXXu0mjfl&o9} literal 0 HcmV?d00001 diff --git a/resources/images/reactions/laugh@3x.png b/resources/images/reactions/laugh@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..01a4ea199c86d387929686e85030df76f2f9b2a8 GIT binary patch literal 7798 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 IM6N<$f^FLjs{jB1 literal 0 HcmV?d00001 diff --git a/resources/images/reactions/love.png b/resources/images/reactions/love.png new file mode 100644 index 0000000000000000000000000000000000000000..6dc171e58b850a65807974b5c14d4b7e5277c1ec GIT binary patch literal 1288 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 LNkvXXu0mjf{jQ4I literal 0 HcmV?d00001 diff --git a/resources/images/reactions/love@3x.png b/resources/images/reactions/love@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..22d033b076b4d8f02566e1f9e73d985556000b3c GIT binary patch literal 6739 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 RP0j!S002ovPDHLkV1idTQWXFI literal 0 HcmV?d00001 diff --git a/resources/images/reactions/sad@2x.png b/resources/images/reactions/sad@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..db786e5142db97f428497a63a6903a5c7ce7c85e GIT binary patch literal 4835 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 JPDHLkV1l6KKBWKv literal 0 HcmV?d00001 diff --git a/resources/images/reactions/sad@3x.png b/resources/images/reactions/sad@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ae9caebd22fff396927706539b51bef089abd498 GIT binary patch literal 8885 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 DVP`%z literal 0 HcmV?d00001 diff --git a/resources/images/reactions/thumbs-down.png b/resources/images/reactions/thumbs-down.png new file mode 100644 index 0000000000000000000000000000000000000000..15a96e735c8d3dda7948602844b834065c261d7a GIT binary patch literal 1332 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 literal 0 HcmV?d00001 diff --git a/resources/images/reactions/thumbs-down@3x.png b/resources/images/reactions/thumbs-down@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..54cacd339af11128b06b33c03f0c9521a29a40f4 GIT binary patch literal 6344 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 KMNUMnLSTZSo%Id? literal 0 HcmV?d00001 diff --git a/resources/images/reactions/thumbs-up.png b/resources/images/reactions/thumbs-up.png new file mode 100644 index 0000000000000000000000000000000000000000..58523f40f229683ce92870413e4f5169b233dd3d GIT binary patch literal 1334 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 V9_wpn450u3002ovPDHLkV1hLRYVH64 literal 0 HcmV?d00001 diff --git a/resources/images/reactions/thumbs-up@3x.png b/resources/images/reactions/thumbs-up@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e39f21d923f05d0eec4b8993edf414844a8c80bb GIT binary patch literal 6396 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 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)) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 73b38abf70..1921c73844 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -1,5 +1,6 @@ (ns status-im.constants (:require [status-im.ethereum.core :as ethereum] + [status-im.react-native.resources :as resources] [status-im.i18n :as i18n] [status-im.utils.config :as config])) @@ -16,6 +17,20 @@ (def content-type-image 7) (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-public-group 2) (def message-type-private-group 3) diff --git a/src/status_im/core.cljs b/src/status_im/core.cljs index 4653916dd0..c0288871a8 100644 --- a/src/status_im/core.cljs +++ b/src/status_im/core.cljs @@ -62,7 +62,7 @@ (.addListener ^js react/keyboard (if platform/ios? "keyboardWillHide" - "keyboardWDidHide") + "keyboardDidHide") (fn [_] (re-frame/dispatch-sync [:set :keyboard-height 0]))) (.addEventListener ^js react/app-state "change" app-state-change-handler) diff --git a/src/status_im/data_store/reactions.cljs b/src/status_im/data_store/reactions.cljs new file mode 100644 index 0000000000..c38be3329d --- /dev/null +++ b/src/status_im/data_store/reactions.cljs @@ -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}]}) diff --git a/src/status_im/ethereum/json_rpc.cljs b/src/status_im/ethereum/json_rpc.cljs index 2f31628ecf..d4a17c48df 100644 --- a/src/status_im/ethereum/json_rpc.cljs +++ b/src/status_im/ethereum/json_rpc.cljs @@ -74,6 +74,9 @@ "shhext_prepareContent" {} "shhext_blockContact" {} "shhext_updateMailservers" {} + "shhext_sendEmojiReaction" {} + "shhext_sendEmojiReactionRetraction" {} + "shhext_emojiReactionsByChatID" {} ;;TODO not used anywhere? "shhext_deleteChat" {} "shhext_saveContact" {} @@ -125,6 +128,9 @@ "wakuext_prepareContent" {} "wakuext_blockContact" {} "wakuext_updateMailservers" {} + "wakuext_sendEmojiReaction" {} + "wakuext_sendEmojiReactionRetraction" {} + "wakuext_emojiReactionsByChatID" {} ;;TODO not used anywhere? "wakuext_deleteChat" {} "wakuext_saveContact" {} diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 1081c42499..8a5d92d8f9 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -7,11 +7,13 @@ [status-im.chat.models.input :as chat.input] [status-im.chat.models.loading :as chat.loading] [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.contact.block :as contact.block] [status-im.contact.core :as contact] [status-im.data-store.chats :as data-store.chats] [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.fleet.core :as fleet] [status-im.group-chats.core :as group-chats] @@ -794,12 +796,14 @@ (fn [_ [_ err]] (log/error :send-status-message-error err))) -(fx/defn handle-update [cofx {:keys [chats messages] :as response}] - (let [chats (map data-store.chats/<-rpc chats) - messages (map data-store.messages/<-rpc messages) - message-fxs (map chat.message/receive-one messages) - chat-fxs (map #(chat/ensure-chat (dissoc % :unviewed-messages-count)) chats)] - (apply fx/merge cofx (concat chat-fxs message-fxs)))) +(fx/defn handle-update [cofx {:keys [chats messages emojiReactions] :as response}] + (let [chats (map data-store.chats/<-rpc chats) + messages (map data-store.messages/<-rpc messages) + message-fxs (map chat.message/receive-one messages) + emoji-reactions (map data-store.reactions/<-rpc emojiReactions) + 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 :transport/message-sent @@ -811,6 +815,16 @@ (conj set-hash-fxs (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 :transport.callback/node-info-fetched (fn [cofx [_ node-info]] diff --git a/src/status_im/react_native/resources.cljs b/src/status_im/react_native/resources.cljs index 5ae71c1a3c..5431cc7451 100644 --- a/src/status_im/react_native/resources.cljs +++ b/src/status_im/react_native/resources.cljs @@ -51,3 +51,11 @@ (get @loaded-images k) (get (swap! loaded-images assoc 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")}) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index d0f9936e98..cdee780de7 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -20,6 +20,7 @@ [status-im.multiaccounts.db :as multiaccounts.db] [status-im.multiaccounts.model :as multiaccounts.model] [status-im.multiaccounts.recover.core :as recover] + [status-im.chat.models.reactions :as models.reactions] [status-im.pairing.core :as pairing] [status-im.signing.gas :as signing.gas] #_[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 ::messages :messages) +(reg-root-key-sub ::reactions :reactions) (reg-root-key-sub ::message-lists :message-lists) (reg-root-key-sub ::pagination-info :pagination-info) @@ -637,6 +639,16 @@ (fn [[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 :chats/messages-gaps :<- [:mailserver/gaps] diff --git a/src/status_im/transport/message/core.cljs b/src/status_im/transport/message/core.cljs index fefe2e0f62..4869452282 100644 --- a/src/status_im/transport/message/core.cljs +++ b/src/status_im/transport/message/core.cljs @@ -2,12 +2,13 @@ status-im.transport.message.core (:require [status-im.chat.models.message :as models.message] [status-im.chat.models :as models.chat] + [status-im.chat.models.reactions :as models.reactions] [status-im.contact.core :as models.contact] [status-im.pairing.core :as models.pairing] [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.chats :as data-store.chats] - [status-im.utils.handlers :as handlers] [status-im.utils.fx :as fx] [status-im.utils.types :as types])) @@ -20,11 +21,17 @@ (fx/defn handle-message [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) ^js contacts (.-contacts response-js) ^js installations (.-installations response-js) - ^js messages (.-messages response-js)] + ^js messages (.-messages response-js) + ^js emoji-reactions (.-emojiReactions response-js)] (cond (seq installations) (let [installations-clj (types/js->clj installations)] @@ -54,12 +61,14 @@ (let [message (.pop messages)] (fx/merge cofx {: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 - ::process - (fn [cofx [_ response-js]] - (process-response cofx response-js))) + (seq emoji-reactions) + (let [reactions (types/js->clj emoji-reactions)] + (js-delete response-js "emojiReactions") + (fx/merge cofx + {:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]} + (handle-reactions (map data-store.reactions/<-rpc reactions))))))) (fx/defn remove-hash [{:keys [db] :as cofx} envelope-hash] diff --git a/src/status_im/transport/message/protocol.cljs b/src/status_im/transport/message/protocol.cljs index 1dddfb4588..5145f8cf35 100644 --- a/src/status_im/transport/message/protocol.cljs +++ b/src/status_im/transport/message/protocol.cljs @@ -31,3 +31,21 @@ :on-success #(re-frame/dispatch [:transport/message-sent % 1]) :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" %)}]}) diff --git a/src/status_im/ui/screens/chat/components/input.cljs b/src/status_im/ui/screens/chat/components/input.cljs index 1a6afdbabf..75e3b31237 100644 --- a/src/status_im/ui/screens/chat/components/input.cljs +++ b/src/status_im/ui/screens/chat/components/input.cljs @@ -134,14 +134,9 @@ :set-active set-active-panel}])]]]]) (defn chat-toolbar [] - (let [text-input-ref (react/create-ref) - 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) + (let [previous-layout (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?]) {:keys [processing]} @(re-frame/subscribe [:multiaccounts/login]) mainnet? @(re-frame/subscribe [:mainnet?]) @@ -151,6 +146,10 @@ public? @(re-frame/subscribe [:current-chat/public?]) reply @(re-frame/subscribe [:chats/reply-message]) 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 ""))) show-send (and (or (not empty-text) sending-image) diff --git a/src/status_im/ui/screens/chat/message/audio.cljs b/src/status_im/ui/screens/chat/message/audio.cljs index f43ba4bc91..345b22100e 100644 --- a/src/status_im/ui/screens/chat/message/audio.cljs +++ b/src/status_im/ui/screens/chat/message/audio.cljs @@ -12,10 +12,6 @@ [status-im.ui.components.react :as react] [status-im.ui.components.slider :as slider])) -(defn message-press-handlers [_] - ;;TBI save audio file? - ) - (defonce player-ref (atom nil)) (defonce current-player-message-id (atom nil)) (defonce current-active-state-ref-ref (atom nil)) diff --git a/src/status_im/ui/screens/chat/message/message.cljs b/src/status_im/ui/screens/chat/message/message.cljs index 8fb53f5ca9..fea4912515 100644 --- a/src/status_im/ui/screens/chat/message/message.cljs +++ b/src/status_im/ui/screens/chat/message/message.cljs @@ -2,10 +2,12 @@ (:require [re-frame.core :as re-frame] [status-im.constants :as constants] [status-im.i18n :as i18n] + [quo.platform :as platform] [status-im.ui.components.colors :as colors] [status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.react :as react] [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.photos :as photos] [status-im.ui.screens.chat.sheets :as sheets] @@ -13,6 +15,7 @@ [status-im.ui.screens.chat.utils :as chat.utils] [status-im.utils.contenthash :as contenthash] [status-im.utils.security :as security] + [status-im.ui.screens.chat.message.reactions :as reactions] [reagent.core :as reagent]) (:require-macros [status-im.utils.views :refer [defview letsubs]])) @@ -29,13 +32,6 @@ outgoing (:rtl? content))} timestamp-str])) -(defn message-bubble-wrapper - [message message-content appender] - [react/view - (style/message-view message) - message-content - appender]) - (defview quoted-message [_ {:keys [from text image]} outgoing current-public-key public?] (letsubs [contact-name [:contacts/contact-name-by-identity from]] @@ -106,14 +102,6 @@ (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 {:keys [type ^js literal children]}] (case type @@ -153,51 +141,15 @@ ;; Append timestamp to new block (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 [{:keys [outgoing content-type content] :as message}] - [message-bubble-wrapper message + [react/view (style/message-view message) [react/text {:style {:color (if outgoing colors/white-persist colors/black)}} (if (seq (:text content)) (:text content) (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 [chat-id message-id] [react/touchable-highlight @@ -220,43 +172,39 @@ (= outgoing-status :not-sent)) [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]] - (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 "Author, userpic and delivery wrapper" [{:keys [alias first-in-group? display-photo? identicon display-username? from outgoing] - :as message} content] - [react/view {:style (style/message-wrapper message) + :as message} content {:keys [modal close-modal]}] + [react/view {:style (style/message-wrapper message) + :pointer-events :box-none :accessibility-label :chat-item} - [react/view (style/message-body message) + [react/view {:style (style/message-body message) + :pointer-events :box-none} (when display-photo? - ; userpic [react/view (style/message-author-userpic outgoing) (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]])]) - ; username - [react/view (style/message-author-wrapper outgoing display-photo?) + [react/view {:style (style/message-author-wrapper outgoing display-photo?)} (when display-username? [react/touchable-opacity {:style style/message-author-touchable - :on-press #(re-frame/dispatch [:chat.ui/show-profile from])} - [message-author-name from alias]]) + :on-press #(do (when modal (close-modal)) + (re-frame/dispatch [:chat.ui/show-profile from]))} + [message-author-name from alias modal]]) ;;MESSAGE CONTENT - content]] + [react/view + content]]] ; delivery status [react/view (style/delivery-status outgoing) [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]}] (let [dimensions (reagent/atom [260 260]) uri (:image content)] @@ -271,57 +219,146 @@ :resize-mode :contain :source {:uri uri}}]]))) -(defn image-message-press-handlers [{:keys [content] :as message}] - {: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}])}) +(defmulti ->message :content-type) -(defn sticker-message-press-handlers [{:keys [content] :as message}] - (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 +(defmethod ->message constants/content-type-command [message] - [react/touchable-highlight (message.audio/message-press-handlers message) - [message-bubble-wrapper message - [message.audio/message-content message [message-timestamp message false]]]]) + [message.command/command-content message-content-wrapper message]) -(defn chat-message [{:keys [public? content content-type] :as message}] - (if (= content-type constants/content-type-command) - [message.command/command-content message-content-wrapper message] - (if (= content-type constants/content-type-system-text) - [system-message-content-wrapper message [system-text-message message]] - [message-content-wrapper - message - (if (= content-type constants/content-type-text) - ;; text message - [text-message message] - (if (= content-type constants/content-type-status) - [message-content-status message] - (if (= content-type constants/content-type-emoji) - [emoji-message message] - (if (= content-type constants/content-type-sticker) - [react/touchable-highlight (sticker-message-press-handlers message) - [react/image {:style {:margin 10 :width 140 :height 140} - ;;TODO (perf) move to event - :source {:uri (contenthash/url (-> content :sticker :hash))}}]] - (if (and (= content-type constants/content-type-image) - ;; Disabling images for public-chats - (not public?)) - [react/touchable-highlight (image-message-press-handlers message) - [message-content-image message]] - (if (and (= content-type constants/content-type-audio) - ;; Disabling audio for public-chats - (not public?)) - [message-content-audio message] - [unknown-content-type message]))))))]))) +(defmethod ->message constants/content-type-system-text [{:keys [content] :as message}] + [react/view {:accessibility-label :chat-item} + [react/view (style/system-message-body message) + [react/view (style/message-view message) + [react/view + [render-parsed-text message (:parsed-text content)]]]]]) + +(defmethod ->message constants/content-type-text + [{:keys [content outgoing current-public-key public?] :as message} {:keys [on-long-press modal] + :as reaction-picker}] + [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) + (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]]] + reaction-picker]) + +(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}]) diff --git a/src/status_im/ui/screens/chat/message/reactions.cljs b/src/status_im/ui/screens/chat/message/reactions.cljs new file mode 100644 index 0000000000..de208d28ee --- /dev/null +++ b/src/status_im/ui/screens/chat/message/reactions.cljs @@ -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}]]]])))) diff --git a/src/status_im/ui/screens/chat/message/reactions_picker.cljs b/src/status_im/ui/screens/chat/message/reactions_picker.cljs new file mode 100644 index 0000000000..fbecd28356 --- /dev/null +++ b/src/status_im/ui/screens/chat/message/reactions_picker.cljs @@ -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}]]]]))))) diff --git a/src/status_im/ui/screens/chat/message/reactions_row.cljs b/src/status_im/ui/screens/chat/message/reactions_row.cljs new file mode 100644 index 0000000000..ec33a0030d --- /dev/null +++ b/src/status_im/ui/screens/chat/message/reactions_row.cljs @@ -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])])) diff --git a/src/status_im/ui/screens/chat/message/styles.cljs b/src/status_im/ui/screens/chat/message/styles.cljs new file mode 100644 index 0000000000..d1e867f8a6 --- /dev/null +++ b/src/status_im/ui/screens/chat/message/styles.cljs @@ -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)"}))) diff --git a/src/status_im/ui/screens/chat/sheets.cljs b/src/status_im/ui/screens/chat/sheets.cljs index 82cd69fb4c..589b1da3fb 100644 --- a/src/status_im/ui/screens/chat/sheets.cljs +++ b/src/status_im/ui/screens/chat/sheets.cljs @@ -152,57 +152,6 @@ :accessibility-label :delete-transaccent-button :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?] (fn [] (let [contact-name @(re-frame/subscribe [:contacts/contact-name-by-identity from])] diff --git a/src/status_im/ui/screens/chat/styles/message/message.cljs b/src/status_im/ui/screens/chat/styles/message/message.cljs index c36866d7e7..a6a8e6545c 100644 --- a/src/status_im/ui/screens/chat/styles/message/message.cljs +++ b/src/status_im/ui/screens/chat/styles/message/message.cljs @@ -10,11 +10,6 @@ [outgoing] {: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 [_] {:margin-top 4 @@ -54,12 +49,8 @@ :bottom 9 ; 6 Bubble bottom, 3 message baseline (if rtl? :left :right) 12}))) -(defn message-wrapper-base [message] - (merge {:flex-direction :column} - (last-message-padding message))) - -(defn message-wrapper [{:keys [outgoing] :as message}] - (merge (message-wrapper-base message) +(defn message-wrapper [{:keys [outgoing]}] + (merge {:flex-direction :column} (if outgoing {:margin-left 96} {:margin-right 52}))) @@ -83,8 +74,8 @@ :padding-left 8})) (def message-author-touchable - {:margin-left 12 - :padding-vertical 2}) + {:margin-left 12 + :flex-direction :row}) (defn message-author-userpic [outgoing] (merge @@ -93,7 +84,7 @@ (if outgoing {:padding-left 8} {:padding-horizontal 8 - :padding-right 8}))) + :padding-right 8}))) (def delivery-text {:color colors/gray @@ -164,13 +155,6 @@ :padding-left 12 :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] {:margin-bottom 6 :padding-bottom 6 diff --git a/src/status_im/ui/screens/chat/styles/photos.cljs b/src/status_im/ui/screens/chat/styles/photos.cljs index c52d46533c..c485377ab6 100644 --- a/src/status_im/ui/screens/chat/styles/photos.cljs +++ b/src/status_im/ui/screens/chat/styles/photos.cljs @@ -18,6 +18,7 @@ :border-radius (radius size)}) (defn photo [size] - {:border-radius (radius size) - :width size - :height size}) + {:border-radius (radius size) + :width size + :height size + :background-color colors/white}) diff --git a/src/status_im/ui/screens/chat/utils.cljs b/src/status_im/ui/screens/chat/utils.cljs index 7d316075bd..3ccccac8c0 100644 --- a/src/status_im/ui/screens/chat/utils.cljs +++ b/src/status_im/ui/screens/chat/utils.cljs @@ -6,26 +6,40 @@ (def ^:private reply-symbol "↪ ") -(defn format-author [contact-name style] - (let [additional-styles (style false)] - (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"} additional-styles)} - (or (stateofus/username trimmed-name) trimmed-name)]) - [react/text {:style (merge {:color colors/gray - :font-size 12 - :line-height 18 - :font-weight "400"} additional-styles)} - contact-name]))) +(defn format-author + ([contact-name] (format-author contact-name false)) + ([contact-name modal] + (if (= (aget contact-name 0) "@") + (let [trimmed-name (subs contact-name 0 81)] + [react/text {:number-of-lines 2 + :style {:color (if modal colors/white-persist colors/blue) + :font-size 13 + :line-height 18 + :font-weight "500"}} + (or (stateofus/username trimmed-name) trimmed-name)]) + [react/text {:style {:color (if modal colors/white-persist colors/gray) + :font-size 12 + :line-height 18 + :font-weight "400"}} + contact-name]))) (defn format-reply-author [from username current-public-key style] - (or (and (= from current-public-key) - [react/text {:style (style true)} - (str reply-symbol (i18n/label :t/You))]) - (format-author (str reply-symbol username) style))) + (let [contact-name (str reply-symbol username)] + (or (and (= from current-public-key) + [react/text {:style (style true)} + (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])))) diff --git a/src/status_im/ui/screens/chat/views.cljs b/src/status_im/ui/screens/chat/views.cljs index 24950488ac..0c9bcba280 100644 --- a/src/status_im/ui/screens/chat/views.cljs +++ b/src/status_im/ui/screens/chat/views.cljs @@ -12,6 +12,7 @@ [quo.animated :as animated] [quo.react-native :as rn] [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.stickers.views :as stickers] [status-im.ui.screens.chat.styles.main :as style] @@ -136,7 +137,7 @@ (debounce/debounce-and-dispatch [:chat.ui/message-visibility-changed e] 5000)) (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] no-messages? [:chats/current-chat-no-messages?] current-public-key [:multiaccount/public-key]] @@ -161,11 +162,13 @@ :incoming-group (and group-chat (not outgoing)) :group-chat group-chat :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-end-reached #(re-frame/dispatch [:chat.ui/load-more-messages]) :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} :keyboardDismissMode "interactive" :keyboard-should-persist-taps :handled})])) @@ -187,12 +190,16 @@ active-panel (reagent/atom nil) position-y (animated/value 0) pan-state (animated/value 0) + text-input-ref (quo.react/create-ref) on-update (partial reset! bottom-space) pan-responder (accessory/create-pan-responder position-y pan-state) set-active-panel (fn [panel] - (reset! active-panel panel) (rn/configure-next (: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 (js/setTimeout #(react/dismiss-keyboard!) 100)))] (fn [] @@ -204,7 +211,7 @@ [react/view {:style {:flex 1}} (when-not group-chat [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? [accessory/view {:y position-y :pan-state pan-state @@ -212,5 +219,6 @@ :on-close #(set-active-panel nil) :on-update-inset on-update} [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]])])))) diff --git a/src/status_im/ui/screens/ens/views.cljs b/src/status_im/ui/screens/ens/views.cljs index 832c278cf4..f0f282d198 100644 --- a/src/status_im/ui/screens/ens/views.cljs +++ b/src/status_im/ui/screens/ens/views.cljs @@ -18,7 +18,6 @@ [status-im.ui.screens.chat.utils :as chat.utils] [status-im.ui.components.toolbar :as toolbar] [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.profile.components.views :as profile.components] [status-im.utils.debounce :as debounce] @@ -629,7 +628,7 @@ (views/defview my-name [] (views/letsubs [contact-name [:multiaccount/preferred-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] [react/view {:style {:flex 1}} @@ -674,9 +673,9 @@ [react/view {:padding-left 72} [my-name]] [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}]] - [message/text-message message]]])]]) + [message/->message message {:on-long-press identity}]]])]]) (views/defview main [] (views/letsubs [{:keys [names multiaccount show? registrations]} [:ens.main/screen]] diff --git a/src/status_im/ui/screens/signing/views.cljs b/src/status_im/ui/screens/signing/views.cljs index d53260f8f9..2024cb5fae 100644 --- a/src/status_im/ui/screens/signing/views.cljs +++ b/src/status_im/ui/screens/signing/views.cljs @@ -316,7 +316,7 @@ (defn error-item [] (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 :flex-direction :row}} [react/text {:style {:color colors/red :margin-right 8}} diff --git a/src/status_im/utils/profiler.clj b/src/status_im/utils/profiler.clj new file mode 100644 index 0000000000..cfcacce7fd --- /dev/null +++ b/src/status_im/utils/profiler.clj @@ -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#)) diff --git a/src/status_im/utils/profiler.cljs b/src/status_im/utils/profiler.cljs new file mode 100644 index 0000000000..61f087250a --- /dev/null +++ b/src/status_im/utils/profiler.cljs @@ -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))) diff --git a/status-go-version.json b/status-go-version.json index 190af1b54b..03c528c89a 100644 --- a/status-go-version.json +++ b/status-go-version.json @@ -2,7 +2,7 @@ "_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh ' instead", "owner": "status-im", "repo": "status-go", - "version": "v0.56.1", - "commit-sha1": "4574ab4c22ee6b662a7f87c39d0f714998c567dc", - "src-sha256": "0jd684lv7x93gxgvvhsv4ihxr5sw2rck2divsxfiql5g2c1v4alg" + "version": "v0.56.5", + "commit-sha1": "ab01a05cd63df0f749862d16d01a90fdd5f694e8", + "src-sha256": "004zjm66gykgz4mlrzsmv797x4pdqjm61dr2dcn1dnbdbdhnglz2" } diff --git a/translations/en.json b/translations/en.json index b7ca530826..1185aab575 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1060,6 +1060,7 @@ "view-etheremon": "View in Etheremon", "view-gitcoin": "View in Gitcoin", "view-profile": "View profile", + "view-details": "View Details", "view-signing": "View signing phrase", "view-superrare": "View in SuperRare", "waiting-for-wifi": "No Wi-fi, message syncing disabled.", diff --git a/yarn.lock b/yarn.lock index 7f4a5160c2..1596448989 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2028,6 +2028,11 @@ bindings@^1.5.0: dependencies: 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: version "0.2.2" 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" 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: version "0.8.3" resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"