From 0cbd3ec805fc46478c8a5f4a55b60690c2c06034 Mon Sep 17 00:00:00 2001 From: yqrashawn Date: Fri, 21 Oct 2022 13:12:40 +0800 Subject: [PATCH] feat: deleted for me message UI (#14168) * feat: delete for me message UI delete and sync deleted for me messages immediately after leaving chat view Signed-off-by: yqrashawn * fix: system message width/height Signed-off-by: yqrashawn Signed-off-by: yqrashawn --- .../components/messages/system_message.cljs | 58 +++++----- .../chat/models/delete_message_for_me.cljs | 28 ++++- .../models/delete_message_for_me_test.cljs | 82 ++++++-------- src/status_im/chat/models/message_list.cljs | 28 +++-- src/status_im/navigation/core.cljs | 13 ++- .../ui2/screens/chat/messages/message.cljs | 105 +++++++++--------- 6 files changed, 163 insertions(+), 151 deletions(-) diff --git a/src/quo2/components/messages/system_message.cljs b/src/quo2/components/messages/system_message.cljs index ef6dc5db55..9140e709f3 100644 --- a/src/quo2/components/messages/system_message.cljs +++ b/src/quo2/components/messages/system_message.cljs @@ -1,14 +1,13 @@ (ns quo2.components.messages.system-message - (:require [status-im.i18n.i18n :as i18n] - [quo.react-native :as rn] - [status-im.utils.core :as utils] + (:require [quo.react-native :as rn] [quo.theme :as theme] - [quo2.components.buttons.button :as button] - [quo2.components.markdown.text :as text] - [quo2.reanimated :as ra] - [quo2.foundations.colors :as colors] + [quo2.components.avatars.icon-avatar :as icon-avatar] [quo2.components.avatars.user-avatar :as user-avatar] - [quo2.components.avatars.icon-avatar :as icon-avatar])) + [quo2.components.markdown.text :as text] + [quo2.foundations.colors :as colors] + [quo2.reanimated :as ra] + [status-im.i18n.i18n :as i18n] + [status-im.utils.core :as utils])) (def themes-landed {:pinned colors/primary-50-opa-5 :added colors/primary-50-opa-5 @@ -55,7 +54,7 @@ (defmulti sm-render :type) -(defmethod sm-render :deleted [{:keys [state action timestamp-str]}] +(defmethod sm-render :deleted [{:keys [label timestamp-str]}] [rn/view {:align-items :center :justify-content :space-between :flex 1 @@ -64,15 +63,12 @@ :flex-direction :row} [sm-icon {:icon :main-icons/delete16 :color :danger - :opacity (if (= state :landed) 0 5)}] + :opacity 5}] [text/text {:size :paragraph-2 :style {:color (get-color :text) :margin-right 5}} - (i18n/label (if action :message-deleted-for-you :message-deleted))] - (when (nil? action) [sm-timestamp timestamp-str])] - (when action [button/button {:size 24 - :before :main-icons/timeout - :type :grey} (i18n/label :undo)])]) + (i18n/label (or label :message-deleted))] + [sm-timestamp timestamp-str]]]) (defmethod sm-render :added [{:keys [state mentions timestamp-str]}] [rn/view {:align-items :center @@ -140,23 +136,27 @@ :style {:color (get-color :time)}} (utils/truncate-str (:info content) 24)])]]]]) -(defn system-message [{:keys [type] :as message}] +(defn system-message + [{:keys [type style non-pressable? animate-landing?] :as message}] [:f> (fn [] - (let [sv-color (ra/use-shared-value (get-color :bg :landed type))] - (ra/animate-shared-value-with-delay - sv-color (get-color :bg :default type) 0 :linear 1000) + (let [sv-color (ra/use-shared-value + (get-color :bg (if animate-landing? :landed :default) type))] + (when animate-landing? + (ra/animate-shared-value-with-delay + sv-color (get-color :bg :default type) 0 :linear 1000)) [ra/touchable-opacity - {:on-press #(ra/set-shared-value - sv-color (get-color :bg :pressed type)) + {:on-press #(when-not non-pressable? + (ra/set-shared-value + sv-color (get-color :bg :pressed type))) :style (ra/apply-animations-to-style {:background-color sv-color} - {:flex-direction :row - :flex 1 - :border-radius 16 - :padding-vertical 9 - :padding-horizontal 11 - :width 359 - :height 52 - :background-color sv-color})} + (merge + {:flex-direction :row + :flex 1 + :border-radius 16 + :padding-vertical 9 + :padding-horizontal 11 + :background-color sv-color} + style))} [sm-render message]]))]) diff --git a/src/status_im/chat/models/delete_message_for_me.cljs b/src/status_im/chat/models/delete_message_for_me.cljs index f9bc72ae82..a275747343 100644 --- a/src/status_im/chat/models/delete_message_for_me.cljs +++ b/src/status_im/chat/models/delete_message_for_me.cljs @@ -1,5 +1,6 @@ (ns status-im.chat.models.delete-message-for-me (:require [re-frame.core :as re-frame] + [status-im.chat.models.message-list :as message-list] [status-im.ethereum.json-rpc :as json-rpc] [status-im.utils.datetime :as datetime] [status-im.utils.fx :as fx] @@ -43,17 +44,22 @@ {:events [:chat.ui/delete-message-for-me]} [{:keys [db]} {:keys [chat-id message-id]} undo-time-limit-ms] (when (get-in db [:messages chat-id message-id]) - {:db (update-db-delete-locally db chat-id message-id undo-time-limit-ms) + (assoc + (message-list/rebuild-message-list + {:db (update-db-delete-locally db chat-id message-id undo-time-limit-ms)} + chat-id) :utils/dispatch-later [{:dispatch [:chat.ui/delete-message-for-me-and-sync {:chat-id chat-id :message-id message-id}] - :ms undo-time-limit-ms}]})) + :ms undo-time-limit-ms}]))) (fx/defn undo {:events [:chat.ui/undo-delete-message-for-me]} [{:keys [db]} {:keys [chat-id message-id]}] (when (get-in db [:messages chat-id message-id]) - {:db (update-db-undo-locally db chat-id message-id)})) + (message-list/rebuild-message-list + {:db (update-db-undo-locally db chat-id message-id)} + chat-id))) (fx/defn delete-and-sync {:events [:chat.ui/delete-message-for-me-and-sync]} @@ -66,3 +72,19 @@ :on-error #(log/error "failed to delete message for me " %) :on-success #(re-frame/dispatch [:sanitize-messages-and-process-response %])}]})) +(defn- chats-reducer + "traverse all messages find not yet synced deleted-for-me? messages, generate dispatch vector" + [acc chat-id messages] + (reduce-kv + (fn [inner-acc message-id {:keys [deleted-for-me? deleted-for-me-undoable-till]}] + (if (and deleted-for-me? deleted-for-me-undoable-till) + (conj inner-acc [:chat.ui/delete-message-for-me-and-sync chat-id message-id]) + inner-acc)) + acc + messages)) + +(fx/defn sync-all + "Get all deleted-for-me messages that not yet synced with status-go and sync them" + {:events [:chat.ui/sync-all-deleted-for-me-messages]} + [{:keys [db]}] + {:dispatch-n (reduce-kv chats-reducer [] (:messages db))}) diff --git a/src/status_im/chat/models/delete_message_for_me_test.cljs b/src/status_im/chat/models/delete_message_for_me_test.cljs index 8c1857af39..2e583745a5 100644 --- a/src/status_im/chat/models/delete_message_for_me_test.cljs +++ b/src/status_im/chat/models/delete_message_for_me_test.cljs @@ -8,65 +8,47 @@ (defonce cid "chat-id") (deftest delete-for-me - (let [db {:messages {cid {mid {:id mid}}}} - message {:message-id mid :chat-id cid}] - (testing "delete for me" - (let [expected {:db {:messages {"chat-id" {"message-id" - {:id "message-id" - :deleted-for-me? true}}}} - :utils/dispatch-later - [{:dispatch [:chat.ui/delete-message-for-me-and-sync - {:chat-id "chat-id" :message-id "message-id"}] - :ms 1000}]} - result (delete-message-for-me/delete {:db db} message 1000) - timestamp (+ (datetime/timestamp) 1000)] - (is (= (update-in result [:db :messages "chat-id" "message-id"] dissoc :deleted-for-me-undoable-till) - expected)) - (is (-> (get-in result [:db :messages "chat-id" "message-id" :deleted-for-me-undoable-till]) - (- timestamp) - js/Math.abs - (< 10))))) - (testing "should return nil if message in db" - (is (= (delete-message-for-me/delete {:db {:messages []}} message 1000) - nil))))) + (with-redefs [datetime/timestamp (constantly 1)] + (let [db {:messages {cid {mid {:id mid :whisper-timestamp 1}}}} + message {:message-id mid :chat-id cid}] + (testing "delete for me" + (let [result-message (get-in (delete-message-for-me/delete {:db db} message 1000) + [:db :messages cid mid])] + (is (= (:id result-message) mid)) + (is (true? (:deleted-for-me? result-message))) + (is (= (:deleted-for-me-undoable-till result-message) 1001)))) + (testing "should return nil if message not in db" + (is (= (delete-message-for-me/delete {:db {:messages []}} message 1000) + nil)))))) (deftest undo-delete-for-me - (let [db {:messages {cid {mid {:id mid}}}} + (let [db {:messages {cid {mid {:id mid :whisper-timestamp 1}}}} message {:message-id mid :chat-id cid}] (testing "undo delete for me in time" - (let [db (update-in db - [:messages cid mid] - assoc - :deleted-for-me? true - :deleted-for-me-undoable-till - (+ (datetime/timestamp) 1000)) + (let [db (update-in db + [:messages cid mid] + assoc + :deleted-for-me? true + :deleted-for-me-undoable-till + (+ (datetime/timestamp) 1000)) + result-message (get-in (delete-message-for-me/undo {:db db} message) + [:db :messages cid mid])] + (is (= (:id result-message) mid)) + (is (nil? (:deleted-for-me? result-message))) + (is (nil? (:deleted-for-me-undoable-till result-message))))) - expected {:db {:messages {"chat-id" {"message-id" - {:id "message-id"}}}}}] - (is (= (delete-message-for-me/undo {:db db} message) expected)))) (testing "remain deleted for me when undo delete for me late" (let [db (update-in db [:messages cid mid] assoc :deleted-for-me? true :deleted-for-me-undoable-till (- (datetime/timestamp) 1000)) + result-message (get-in (delete-message-for-me/undo {:db db} message) [:db :messages cid mid])] + (is (= (:id result-message) mid)) + (is (nil? (:deleted-for-me-undoable-till result-message))) + (is (true? (:deleted-for-me? result-message))))) - expected {:db {:messages {"chat-id" {"message-id" - {:id "message-id" - :deleted-for-me? true}}}}}] - (is (= (delete-message-for-me/undo {:db db} message) expected)))) - (testing "remain deleted for me when undo delete for me late" - (let [db (update-in db - [:messages cid mid] - assoc - :deleted-for-me? true - :deleted-for-me-undoable-till (- (datetime/timestamp) 1000)) - - expected {:db {:messages {"chat-id" {"message-id" - {:id "message-id" - :deleted-for-me? true}}}}}] - (is (= (delete-message-for-me/undo {:db db} message) expected)))) - (testing "should return nil if message in db" + (testing "should return nil if message not in db" (is (= (delete-message-for-me/undo {:db {:messages []}} message) nil))))) @@ -74,7 +56,7 @@ (let [db {:messages {cid {mid {:id mid}}}} message {:message-id mid :chat-id cid}] (testing "delete for me and sync" - (let [expected-db {:messages {"chat-id" {"message-id" {:id "message-id"}}}} + (let [expected-db {:messages {cid {mid {:id mid}}}} effects (delete-message-for-me/delete-and-sync {:db db} message) result-db (:db effects) rpc-calls (:status-im.ethereum.json-rpc/call effects)] @@ -100,7 +82,7 @@ second) mid)))) (testing "delete for me and sync, should clean undo timer" - (let [expected-db {:messages {"chat-id" {"message-id" {:id "message-id"}}}} + (let [expected-db {:messages {cid {mid {:id mid}}}} effects (delete-message-for-me/delete-and-sync {:db (update-in db [:messages cid mid @@ -109,7 +91,7 @@ message) result-db (:db effects)] (is (= result-db expected-db)))) - (testing "should return nil if message in db" + (testing "should return nil if message not in db" (is (= (delete-message-for-me/delete-and-sync {:db {:messages []}} message) nil))))) diff --git a/src/status_im/chat/models/message_list.cljs b/src/status_im/chat/models/message_list.cljs index 460d2bc9ce..493ae56f01 100644 --- a/src/status_im/chat/models/message_list.cljs +++ b/src/status_im/chat/models/message_list.cljs @@ -1,8 +1,8 @@ (ns status-im.chat.models.message-list - (:require [status-im.constants :as constants] - [status-im.utils.fx :as fx] + (:require ["functional-red-black-tree" :as rb-tree] + [status-im.constants :as constants] [status-im.utils.datetime :as time] - ["functional-red-black-tree" :as rb-tree])) + [status-im.utils.fx :as fx])) (defn- add-datemark [{:keys [whisper-timestamp] :as msg}] ;;TODO this is slow @@ -16,16 +16,20 @@ message-type from outgoing - whisper-timestamp]}] + whisper-timestamp + deleted-for-me?]}] (-> {:whisper-timestamp whisper-timestamp - :from from - :one-to-one? (= constants/message-type-one-to-one message-type) - :system-message? (= constants/message-type-private-group-system-message - message-type) - :clock-value clock-value - :type :message - :message-id message-id - :outgoing (boolean outgoing)} + :from from + :one-to-one? (= constants/message-type-one-to-one message-type) + :system-message? (boolean + (or + (= constants/message-type-private-group-system-message + message-type) + deleted-for-me?)) + :clock-value clock-value + :type :message + :message-id message-id + :outgoing (boolean outgoing)} add-datemark add-timestamp)) diff --git a/src/status_im/navigation/core.cljs b/src/status_im/navigation/core.cljs index 9a1cf69cc3..d11343d9e7 100644 --- a/src/status_im/navigation/core.cljs +++ b/src/status_im/navigation/core.cljs @@ -1,21 +1,22 @@ (ns status-im.navigation.core (:require ["react-native" :as rn] - [clojure.set :as clojure.set] ["react-native-gesture-handler" :refer (gestureHandlerRootHOC)] ["react-native-navigation" :refer (Navigation)] + [clojure.set :as clojure.set] [quo.components.text-input :as quo.text-input] [quo.design-system.colors :as quo.colors] [re-frame.core :as re-frame] + [status-im.multiaccounts.login.core :as login-core] [status-im.navigation.roots :as roots] + [status-im.navigation.state :as state] [status-im.ui.components.icons.icons :as icons] [status-im.ui.components.react :as react] [status-im.ui.screens.views :as views] [status-im.utils.fx :as fx] [status-im.utils.platform :as platform] - [taoensso.timbre :as log] - [status-im.multiaccounts.login.core :as login-core] - [status-im.navigation.state :as state])) + [taoensso.encore :as enc] + [taoensso.timbre :as log])) (def debug? ^boolean js/goog.DEBUG) @@ -438,4 +439,6 @@ :community :else - :home))})) + :home)) + :dispatch-n (enc/conj-when [] + (and (= view-id :chat) [:chat.ui/sync-all-deleted-for-me-messages]))})) diff --git a/src/status_im/ui2/screens/chat/messages/message.cljs b/src/status_im/ui2/screens/chat/messages/message.cljs index c420141290..8d7c27986a 100644 --- a/src/status_im/ui2/screens/chat/messages/message.cljs +++ b/src/status_im/ui2/screens/chat/messages/message.cljs @@ -1,6 +1,10 @@ (ns status-im.ui2.screens.chat.messages.message (:require [quo.core :as quo] [quo.design-system.colors :as colors] + [quo.react-native :as rn] + [quo2.components.messages.system-message :as system-message] + [quo2.foundations.colors :as quo2.colors] + [quo2.foundations.typography :as typography] [re-frame.core :as re-frame] [reagent.core :as reagent] [status-im.chat.models.delete-message-for-me] @@ -11,11 +15,12 @@ [status-im.i18n.i18n :as i18n] [status-im.react-native.resources :as resources] [status-im.ui.components.animation :as animation] + [status-im.ui.components.chat-icon.screen :as chat-icon] [status-im.ui.components.fast-image :as fast-image] [status-im.ui.components.icons.icons :as icons] + [status-im.ui.components.list.views :as list] [status-im.ui.components.react :as react] [status-im.ui.screens.chat.bottom-sheets.context-drawer :as message-context-drawer] - [status-im.ui2.screens.chat.components.reply :as components.reply] [status-im.ui.screens.chat.image.preview.views :as preview] [status-im.ui.screens.chat.message.audio :as message.audio] [status-im.ui.screens.chat.message.command :as message.command] @@ -26,17 +31,13 @@ [status-im.ui.screens.chat.photos :as photos] [status-im.ui.screens.chat.sheets :as sheets] [status-im.ui.screens.chat.styles.message.message :as style] - [status-im.ui.screens.chat.utils :as chat.utils] [status-im.ui.screens.chat.styles.photos :as photos.style] + [status-im.ui.screens.chat.utils :as chat.utils] [status-im.ui.screens.communities.icon :as communities.icon] - [status-im.utils.handlers :refer [evt]] + [status-im.ui2.screens.chat.components.reply :as components.reply] [status-im.utils.config :as config] - [status-im.utils.security :as security] - [quo2.foundations.typography :as typography] - [quo2.foundations.colors :as quo2.colors] - [status-im.ui.components.list.views :as list] - [quo.react-native :as rn] - [status-im.ui.components.chat-icon.screen :as chat-icon]) + [status-im.utils.handlers :refer [evt]] + [status-im.utils.security :as security]) (:require-macros [status-im.utils.views :refer [defview letsubs]])) (defn message-timestamp-anim @@ -263,7 +264,7 @@ (defview community-content [{:keys [community-id] :as message}] (letsubs [{:keys [name description verified] :as community} [:communities/community community-id] - communities-enabled? [:communities/enabled?]] + communities-enabled? [:communities/enabled?]] (when (and communities-enabled? community) [rn/view {:style (assoc (style/message-wrapper message) :margin-vertical 10 @@ -294,51 +295,51 @@ (defn message-content-wrapper "Author, userpic and delivery wrapper" - [{:keys [last-in-group? - identicon - from in-popover? timestamp-str - deleted-for-me? pinned] + [{:keys [last-in-group? identicon from in-popover? timestamp-str + deleted-for-me? deleted-for-me-undoable-till pinned] :as message} content {:keys [modal close-modal]}] (let [response-to (:response-to (:content message))] - [rn/view {:style (style/message-wrapper message) - :pointer-events :box-none - :accessibility-label :chat-item} - (when (and (seq response-to) (:quoted-message message)) - [quoted-message response-to (:quoted-message message)]) - [rn/view {:style (style/message-body) - :pointer-events :box-none} - [rn/view (style/message-author-userpic) - (when (or (and (seq response-to) (:quoted-message message)) last-in-group? pinned) - [rn/touchable-highlight {:on-press #(do (when modal (close-modal)) - (re-frame/dispatch [:chat.ui/show-profile from]))} - [photos/member-photo from identicon]])] + (if deleted-for-me? + [system-message/system-message + {:type :deleted + :label :message-deleted-for-you + :timestamp-str timestamp-str + :non-pressable? true + :animate-landing? (if deleted-for-me-undoable-till true false)}] + [rn/view {:style (style/message-wrapper message) + :pointer-events :box-none + :accessibility-label :chat-item} + (when (and (seq response-to) (:quoted-message message)) + [quoted-message response-to (:quoted-message message)]) + [rn/view {:style (style/message-body) + :pointer-events :box-none} + [rn/view (style/message-author-userpic) + (when (or (and (seq response-to) (:quoted-message message)) last-in-group? pinned) + [rn/touchable-highlight {:on-press #(do (when modal (close-modal)) + (re-frame/dispatch [:chat.ui/show-profile from]))} + [photos/member-photo from identicon]])] - [rn/view {:style (style/message-author-wrapper)} - (when (or (and (seq response-to) (:quoted-message message)) last-in-group? pinned) - [rn/view {:style {:flex-direction :row :align-items :center}} - [rn/touchable-opacity {:style style/message-author-touchable - :disabled in-popover? - :on-press #(do (when modal (close-modal)) - (re-frame/dispatch [:chat.ui/show-profile from]))} - [message-author-name from {:modal modal}]] - [rn/text - {:style (merge - {:padding-left 5 - :margin-top 2} - (style/message-timestamp-text)) - :accessibility-label :message-timestamp} - timestamp-str]]) - ;; MESSAGE CONTENT - ;; TODO(yqrashawn): wait for system message component to display deleted for me UI - (if deleted-for-me? - [rn/view {:style {:border-width 2 - :border-color :red}} - content] - content) - [link-preview/link-preview-wrapper (:links (:content message)) false false]]] - ; delivery status - [rn/view (style/delivery-status) - [message-delivery-status message]]])) + [rn/view {:style (style/message-author-wrapper)} + (when (or (and (seq response-to) (:quoted-message message)) last-in-group? pinned) + [rn/view {:style {:flex-direction :row :align-items :center}} + [rn/touchable-opacity {:style style/message-author-touchable + :disabled in-popover? + :on-press #(do (when modal (close-modal)) + (re-frame/dispatch [:chat.ui/show-profile from]))} + [message-author-name from {:modal modal}]] + [rn/text + {:style (merge + {:padding-left 5 + :margin-top 2} + (style/message-timestamp-text)) + :accessibility-label :message-timestamp} + timestamp-str]]) + ;; MESSAGE CONTENT + content + [link-preview/link-preview-wrapper (:links (:content message)) false false]]] + ;; delivery status + [rn/view (style/delivery-status) + [message-delivery-status message]]]))) (def image-max-width 260) (def image-max-height 192)