From 02a1c3597f0f775bb1c9dd358ef655ea56b9b590 Mon Sep 17 00:00:00 2001 From: yqrashawn Date: Tue, 10 Jan 2023 10:02:23 +0800 Subject: [PATCH] feat: undo delete with toast (#14618) --- src/mocks/js_dependencies.cljs | 106 ++++++++--------- src/quo2/components/notifications/toast.cljs | 28 +++-- src/react_native/core.cljs | 2 +- src/react_native/reanimated.cljs | 5 +- src/status_im/chat/models/message_list.cljs | 6 +- src/status_im2/common/toasts/view.cljs | 21 ++-- .../chat/messages/content/deleted/view.cljs | 4 +- .../chat/messages/delete_message/events.cljs | 107 +++++++++++++----- .../messages/delete_message/events_test.cljs | 101 +++++++++++------ .../delete_message_for_me/events.cljs | 91 +++++++++++---- .../delete_message_for_me/events_test.cljs | 91 ++++++++++----- src/status_im2/setup/db.cljs | 1 + src/status_im2/subs/toasts.cljs | 6 + translations/en.json | 8 ++ 14 files changed, 382 insertions(+), 195 deletions(-) diff --git a/src/mocks/js_dependencies.cljs b/src/mocks/js_dependencies.cljs index ee07cf562e..372856a5da 100644 --- a/src/mocks/js_dependencies.cljs +++ b/src/mocks/js_dependencies.cljs @@ -206,58 +206,60 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return ( (def react-native-reanimated #js - {:default #js - {:createAnimatedComponent identity - :eq nil - :greaterOrEq nil - :greaterThan nil - :lessThan nil - :lessOrEq nil - :add nil - :diff nil - :divide nil - :sub nil - :multiply nil - :abs nil - :min nil - :max nil - :neq nil - :and nil - :or nil - :not nil - :set nil - :startClock nil - :stopClock nil - :Value nil - :Clock nil - :debug nil - :log nil - :event nil - :cond nil - :block nil - :interpolateNode nil - :call nil - :timing nil - :onChange nil - :View #js {} - :Image #js {} - :ScrollView #js {} - :Text #js {} - :Extrapolate #js {:CLAMP nil} - :Code #js {}} - :EasingNode #js - {:bezier identity - :linear identity} - :clockRunning nil - :useSharedValue (fn []) - :useAnimatedStyle (fn []) - :withTiming (fn []) - :withDelay (fn []) - :Easing #js {:bezier identity} - :Keyframe (fn []) - :SlideOutUp js/__STATUS_MOBILE_JS_IDENTITY_PROXY__ - :SlideInUp js/__STATUS_MOBILE_JS_IDENTITY_PROXY__ - :LinearTransition js/__STATUS_MOBILE_JS_IDENTITY_PROXY__}) + {:default #js + {:createAnimatedComponent identity + :eq nil + :greaterOrEq nil + :greaterThan nil + :lessThan nil + :lessOrEq nil + :add nil + :diff nil + :divide nil + :sub nil + :multiply nil + :abs nil + :min nil + :max nil + :neq nil + :and nil + :or nil + :not nil + :set nil + :startClock nil + :stopClock nil + :Value nil + :Clock nil + :debug nil + :log nil + :event nil + :cond nil + :block nil + :interpolateNode nil + :call nil + :timing nil + :onChange nil + :View #js {} + :Image #js {} + :ScrollView #js {} + :Text #js {} + :Extrapolate #js {:CLAMP nil} + :Code #js {}} + :EasingNode #js + {:bezier identity + :linear identity} + :clockRunning nil + :useSharedValue (fn []) + :useAnimatedStyle (fn []) + :withTiming (fn []) + :withDelay (fn []) + :Easing #js {:bezier identity} + :Keyframe (fn []) + :enableLayoutAnimations (fn []) + :SlideOutUp js/__STATUS_MOBILE_JS_IDENTITY_PROXY__ + :SlideInUp js/__STATUS_MOBILE_JS_IDENTITY_PROXY__ + :LinearTransition js/__STATUS_MOBILE_JS_IDENTITY_PROXY__}) + (def react-native-gesture-handler #js {:default #js {} diff --git a/src/quo2/components/notifications/toast.cljs b/src/quo2/components/notifications/toast.cljs index 4dda963b15..dd5f4112cd 100644 --- a/src/quo2/components/notifications/toast.cljs +++ b/src/quo2/components/notifications/toast.cljs @@ -8,14 +8,14 @@ [react-native.core :as rn])) (def ^:private themes - {:container {:light {:background-color colors/white-opa-70} - :dark {:background-color colors/neutral-80-opa-70}} - :text {:light {:color colors/neutral-100} - :dark {:color colors/white}} - :icon {:light {:color colors/neutral-100} - :dark {:color colors/white}} - :action-container {:light {:background-color :colors/neutral-80-opa-5} - :dark {:background-color :colors/white-opa-5}}}) + {:container {:dark {:background-color colors/white-opa-70} + :light {:background-color colors/neutral-80-opa-70}} + :text {:dark {:color colors/neutral-100} + :light {:color colors/white}} + :icon {:dark {:color colors/neutral-100} + :light {:color colors/white}} + :action-container {:dark {:background-color :colors/neutral-80-opa-5} + :light {:background-color :colors/white-opa-5}}}) (defn- merge-theme-style [component-key styles] @@ -23,7 +23,9 @@ (defn toast-action-container [{:keys [on-press style]} & children] - [rn/touchable-highlight {:on-press on-press} + [rn/touchable-highlight + {:on-press on-press + :underlay-color :transparent} [into [rn/view {:style (merge @@ -40,7 +42,8 @@ (defn toast-undo-action [duration on-press] - [toast-action-container {:on-press on-press} + [toast-action-container + {:on-press on-press :accessibility-label :toast-undo-action} [rn/view {:style {:margin-right 5}} [count-down-circle/circle-timer {:duration duration}]] [text/text @@ -63,7 +66,10 @@ [rn/view {:style {:padding 2}} left] [rn/view {:style {:padding 4 :flex 1}} [text/text - {:size :paragraph-2 :weight :medium :style (merge-theme-style :text {})} + {:size :paragraph-2 + :weight :medium + :style (merge-theme-style :text {}) + :accessibility-label :toast-content} middle]] (when right right)]]) diff --git a/src/react_native/core.cljs b/src/react_native/core.cljs index bc32b2ddbc..ad78f4a5f5 100644 --- a/src/react_native/core.cljs +++ b/src/react_native/core.cljs @@ -101,4 +101,4 @@ [f] (let [fn-ref (use-ref f)] (oops/oset! fn-ref "current" f) - (use-effect-once (fn [] #((oops/oget fn-ref "current")))))) + (use-effect-once (fn [] (fn [] (oops/ocall! fn-ref "current")))))) diff --git a/src/react_native/reanimated.cljs b/src/react_native/reanimated.cljs index f397ff67f6..4070d778d4 100644 --- a/src/react_native/reanimated.cljs +++ b/src/react_native/reanimated.cljs @@ -12,10 +12,13 @@ cancelAnimation SlideInUp SlideOutUp - LinearTransition)] + LinearTransition + enableLayoutAnimations)] [clojure.string :as string] [reagent.core :as reagent])) +(enableLayoutAnimations true) + ;; Animations (def slide-in-up-animation SlideInUp) (def slide-out-up-animation SlideOutUp) diff --git a/src/status_im/chat/models/message_list.cljs b/src/status_im/chat/models/message_list.cljs index f27d9b470a..a5a80cbf36 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 ["functional-red-black-tree" :as rb-tree] [status-im.constants :as constants] - [utils.re-frame :as rf] - [utils.datetime :as datetime])) + [utils.datetime :as datetime] + [utils.re-frame :as rf])) (defn- add-datemark [{:keys [whisper-timestamp] :as msg}] @@ -20,6 +20,7 @@ from outgoing whisper-timestamp + deleted? deleted-for-me? albumize?]}] (-> {:whisper-timestamp whisper-timestamp @@ -29,6 +30,7 @@ (or (= constants/message-type-private-group-system-message message-type) + deleted? deleted-for-me?)) :clock-value clock-value :type :message diff --git a/src/status_im2/common/toasts/view.cljs b/src/status_im2/common/toasts/view.cljs index f62a50f44d..4fed016fa5 100644 --- a/src/status_im2/common/toasts/view.cljs +++ b/src/status_im2/common/toasts/view.cljs @@ -26,6 +26,11 @@ (.damping 20) (.stiffness 300))) +(defn toast + [id] + (let [toast-opts (rf/sub [:toasts/toast id])] + [quo/toast toast-opts])) + (defn container [id] (let [dismissed-locally? (reagent/atom false) @@ -35,13 +40,11 @@ (fn [] [:f> (fn [] - (let [toast-opts (rf/sub [:toasts/toast id]) - duration (get toast-opts :duration 3000) - on-dismissed #((get toast-opts :on-dismissed identity) id) - translate-y (reanimated/use-shared-value 0) + (let [duration (or (rf/sub [:toasts/toast-cursor id :duration]) 3000) + on-dismissed #((or (rf/sub [:toasts/toast-cursor id :on-dismissed]) identity) id) create-timer (fn [] - (reset! timer (utils.utils/set-timeout #(do (close!) (on-dismissed)) - duration))) + (reset! timer (utils.utils/set-timeout close! duration))) + translate-y (reanimated/use-shared-value 0) pan (-> (gesture/gesture-pan) @@ -84,7 +87,7 @@ :style (reanimated/apply-animations-to-style {:transform [{:translateY translate-y}]} style/each-toast-container)} - [quo/toast toast-opts]]]))]))) + [toast id]]]))]))) (defn toasts [] @@ -92,6 +95,4 @@ [into [rn/view {:style style/outmost-transparent-container}] - (->> toasts-ordered - reverse - (map (fn [id] ^{:key id} [container id])))])) + (map (fn [id] ^{:key id} [container id]) toasts-ordered)])) diff --git a/src/status_im2/contexts/chat/messages/content/deleted/view.cljs b/src/status_im2/contexts/chat/messages/content/deleted/view.cljs index b307c9987a..99910a25c3 100644 --- a/src/status_im2/contexts/chat/messages/content/deleted/view.cljs +++ b/src/status_im2/contexts/chat/messages/content/deleted/view.cljs @@ -1,6 +1,6 @@ (ns status-im2.contexts.chat.messages.content.deleted.view - (:require [quo2.core :as quo] - [i18n.i18n :as i18n])) + (:require [i18n.i18n :as i18n] + [quo2.core :as quo])) (defn deleted-message [{:keys [deleted? deleted-undoable-till timestamp-str deleted-for-me-undoable-till]}] diff --git a/src/status_im2/contexts/chat/messages/delete_message/events.cljs b/src/status_im2/contexts/chat/messages/delete_message/events.cljs index ca2f667f08..bd8f2ffc7b 100644 --- a/src/status_im2/contexts/chat/messages/delete_message/events.cljs +++ b/src/status_im2/contexts/chat/messages/delete_message/events.cljs @@ -1,8 +1,11 @@ (ns status-im2.contexts.chat.messages.delete-message.events - (:require [status-im.chat.models.message-list :as message-list] - [utils.datetime :as datetime] - [taoensso.timbre :as log] - [utils.re-frame :as rf])) + (:require + [i18n.i18n :as i18n] + [quo2.foundations.colors :as colors] + [status-im.chat.models.message-list :as message-list] + [taoensso.timbre :as log] + [utils.datetime :as datetime] + [utils.re-frame :as rf])) (defn- update-db-clear-undo-timer [db chat-id message-id] @@ -20,8 +23,7 @@ [:messages chat-id message-id] assoc :deleted? true - :deleted-undoable-till (+ (datetime/timestamp) - undo-time-limit-ms)))) + :deleted-undoable-till (+ (datetime/timestamp) undo-time-limit-ms)))) (defn- update-db-undo-locally "Restore deleted message if called within timelimit" @@ -48,15 +50,43 @@ {:events [:chat.ui/delete-message]} [{:keys [db]} {:keys [chat-id message-id]} undo-time-limit-ms] (when (get-in db [:messages chat-id message-id]) - (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-and-send - {:chat-id chat-id - :message-id message-id}] - :ms undo-time-limit-ms}]))) + ;; all delete message toast are the same toast with id :delete-message-for-everyone + ;; new delete operation will reset prev pending deletes' undo timelimit + ;; undo will undo all pending deletes + ;; all pending deletes are stored in toast + (let [existing-undo-toast (get-in db [:toasts :toasts :delete-message-for-everyone]) + toast-count (inc (get existing-undo-toast :message-deleted-for-everyone-count 0)) + existing-undos (-> existing-undo-toast + (get :message-deleted-for-everyone-undos []) + (conj {:message-id message-id :chat-id chat-id}))] + (assoc + (message-list/rebuild-message-list + {:db (reduce + ;; sync all pending deletes' undo timelimit, extend to the latest one + (fn [db-acc {:keys [chat-id message-id]}] + (update-db-delete-locally db-acc chat-id message-id undo-time-limit-ms)) + db + existing-undos)} + chat-id) + + :dispatch-n + [[:toasts/close :delete-message-for-everyone] + [:toasts/upsert :delete-message-for-everyone + {:icon :info + :icon-color colors/danger-50-opa-40 + :message-deleted-for-everyone-count toast-count + :message-deleted-for-everyone-undos existing-undos + :text (i18n/label-pluralize + toast-count + :t/message-deleted-for-everyone-count) + :duration undo-time-limit-ms + :undo-duration (/ undo-time-limit-ms 1000) + :undo-on-press #(do (rf/dispatch [:chat.ui/undo-all-delete-message]) + (rf/dispatch [:toasts/close + :delete-message-for-everyone]))}]] + :utils/dispatch-later [{:dispatch [:chat.ui/delete-message-and-send + {:chat-id chat-id :message-id message-id}] + :ms undo-time-limit-ms}])))) (rf/defn undo {:events [:chat.ui/undo-delete-message]} @@ -66,22 +96,41 @@ {:db (update-db-undo-locally db chat-id message-id)} chat-id))) +(rf/defn undo-all + {:events [:chat.ui/undo-all-delete-message]} + [{:keys [db]}] + (when-let [pending-undos (get-in db + [:toasts :toasts :delete-message-for-everyone + :message-deleted-for-everyone-undos])] + {:dispatch-n (mapv #(vector :chat.ui/undo-delete-message %) pending-undos)})) + +(defn- check-before-delete-and-send + "make sure message alredy deleted? locally and undo timelimit has passed" + [db chat-id message-id] + (let [message (get-in db [:messages chat-id message-id]) + {:keys [deleted? deleted-undoable-till]} message] + (and deleted? + deleted-undoable-till + (>= (datetime/timestamp) deleted-undoable-till)))) + (rf/defn delete-and-send {:events [:chat.ui/delete-message-and-send]} - [{:keys [db]} {:keys [message-id chat-id]}] + [{:keys [db]} {:keys [message-id chat-id]} force?] (when-let [message (get-in db [:messages chat-id message-id])] - (cond-> {:db (update-db-clear-undo-timer db chat-id message-id) - :json-rpc/call [{:method "wakuext_deleteMessageAndSend" - :params [message-id] - :js-response true - :on-error #(log/error "failed to delete message " - {:message-id message-id :error %}) - :on-success #(rf/dispatch [:sanitize-messages-and-process-response - %])}]} - (get-in db [:pin-messages chat-id message-id]) - (assoc :dispatch - [:pin-message/send-pin-message - {:chat-id chat-id :message-id message-id :pinned false}])))) + (when (or force? (check-before-delete-and-send db chat-id message-id)) + (cond-> {:db (update-db-clear-undo-timer db chat-id message-id) + :json-rpc/call [{:method "wakuext_deleteMessageAndSend" + :params [message-id] + :js-response true + :on-error #(log/error "failed to delete message " + {:message-id message-id :error %}) + :on-success #(rf/dispatch + [:sanitize-messages-and-process-response + %])}]} + (get-in db [:pin-messages chat-id message-id]) + (assoc :dispatch + [:pin-message/send-pin-message + {:chat-id chat-id :message-id message-id :pinned false}]))))) (defn- filter-pending-send-messages "traverse all messages find not yet synced deleted? messages" @@ -96,7 +145,7 @@ {:events [:chat.ui/send-all-deleted-messages]} [{:keys [db] :as cofx}] (let [pending-send-messages (reduce-kv filter-pending-send-messages [] (:messages db))] - (apply rf/merge cofx (map delete-and-send pending-send-messages)))) + (apply rf/merge cofx (map #(delete-and-send % true) pending-send-messages)))) (rf/defn delete-messages-localy "Mark messages :deleted? localy in client" diff --git a/src/status_im2/contexts/chat/messages/delete_message/events_test.cljs b/src/status_im2/contexts/chat/messages/delete_message/events_test.cljs index bf09320251..6361b728c8 100644 --- a/src/status_im2/contexts/chat/messages/delete_message/events_test.cljs +++ b/src/status_im2/contexts/chat/messages/delete_message/events_test.cljs @@ -15,11 +15,46 @@ (let [result-message (get-in (delete-message/delete {:db db} message 1000) [:db :messages cid mid])] (is (= (:id result-message) mid)) - (is (true? (:deleted? result-message))) - (is (= (:deleted-undoable-till result-message) 1001)))) + (is (true? (:deleted? result-message)) "mark message :deleted?") + (is (= (:deleted-undoable-till result-message) 1001) "set message undo timelimit"))) + (testing "delete with pending deletes" + (let [db (-> db + (update-in [:messages cid "pending-delete-message"] + assoc + :deleted? true + :deleted-undoable-till 0 + :whisper-timestamp 0) + (update-in [:toasts :toasts :delete-message-for-everyone] + assoc + :message-deleted-for-everyone-count 1 + :message-deleted-for-everyone-undos [{:message-id + "pending-delete-message" + :chat-id cid}])) + effects (delete-message/delete {:db db} message 1000)] + (is (= (get-in effects [:db :messages cid mid :deleted-undoable-till]) + (get-in effects [:db :messages cid "pending-delete-message" :deleted-undoable-till]) + 1001) + "sync all pending delete undo timelimit") + (let [upsert-toast (-> effects :dispatch-n second)] + (is (= (-> upsert-toast last :message-deleted-for-everyone-count) 2) + "+1 pending deletes") + (is + (and + (-> upsert-toast + last + :message-deleted-for-everyone-undos + first + :message-id + (= "pending-delete-message")) + (-> upsert-toast + last + :message-deleted-for-everyone-undos + second + :message-id + (= mid))) + "pending deletes are in order")))) (testing "return nil if message not in db" - (is (= (delete-message/delete {:db {:messages []}} message 1000) - nil))))))) + (is (= (delete-message/delete {:db {:messages []}} message 1000) nil))))))) (deftest undo-delete (let [db {:messages {cid {mid {:id mid :whisper-timestamp 1}}}} @@ -30,14 +65,11 @@ [:messages cid mid] assoc :deleted? true - :deleted-undoable-till - (+ (datetime/timestamp) 1000)) - result-message (get-in (delete-message/undo {:db db} message) - [:db :messages cid mid])] + :deleted-undoable-till (+ (datetime/timestamp) 1000)) + result-message (get-in (delete-message/undo {:db db} message) [:db :messages cid mid])] (is (= (:id result-message) mid)) (is (nil? (:deleted? result-message))) (is (nil? (:deleted-undoable-till result-message))))) - (testing "remain deleted when undo after timelimit" (let [db (update-in db [:messages cid mid] @@ -48,47 +80,46 @@ (is (= (:id result-message) mid)) (is (nil? (:deleted-undoable-till result-message))) (is (true? (:deleted? result-message))))) - (testing "return nil if message not in db" - (is (= (delete-message/undo {:db {:messages []}} message) - nil)))))) + (is (= (delete-message/undo {:db {:messages []}} message) nil)))))) (deftest delete-and-send - (let [db {:messages {cid {mid {:id mid}}}} + (let [db {:messages {cid {mid {:id mid :deleted? true :deleted-undoable-till 0}}}} message {:message-id mid :chat-id cid}] (testing "delete and send" (testing "dispatch right rpc call fx" - (let [expected-db {:messages {cid {mid {:id mid}}}} - effects (delete-message/delete-and-send {:db db} message) + (let [expected-db {:messages {cid {mid {:id mid :deleted? true}}}} + effects (delete-message/delete-and-send {:db db} message false) result-db (:db effects) rpc-calls (:json-rpc/call effects)] (is (= result-db expected-db)) (is (= (count rpc-calls) 1)) - (is (= (-> rpc-calls - first - :method) - "wakuext_deleteMessageAndSend")) - (is (= (-> rpc-calls - first - :params - count) - 1)) - (is (= (-> rpc-calls - first - :params - first) - mid)))) + (is (= (-> rpc-calls first :method) "wakuext_deleteMessageAndSend")) + (is (= (-> rpc-calls first :params count) 1)) + (is (= (-> rpc-calls first :params first) mid)))) (testing "clean undo timer" - (let [expected-db {:messages {cid {mid {:id mid}}}} + (let [expected-db {:messages {cid {mid {:id mid :deleted? true}}}} effects (delete-message/delete-and-send {:db (update-in db - [:messages cid mid - :deleted-undoable-till] + [:messages cid mid :deleted-undoable-till] (constantly (datetime/timestamp)))} - message) + message + false) result-db (:db effects)] (is (= result-db expected-db)))) + (testing "before deleted locally" + (let [effects (delete-message/delete-and-send + {:db (update-in db [:messages cid mid] dissoc :deleted?)} + message + false)] + (is (-> effects :db nil?) "not delete and send"))) + (testing "before undo timelimit" + (with-redefs [datetime/timestamp (constantly 1)] + (let [effects (delete-message/delete-and-send + {:db (update-in db [:messages cid mid] assoc :deleted-undoable-till 2)} + message + false)] + (is (-> effects :db nil?))))) (testing "return nil if message not in db" - (is (= (delete-message/delete-and-send {:db {:messages []}} - message) + (is (= (delete-message/delete-and-send {:db {:messages []}} message false) nil)))))) diff --git a/src/status_im2/contexts/chat/messages/delete_message_for_me/events.cljs b/src/status_im2/contexts/chat/messages/delete_message_for_me/events.cljs index b0aeeaee53..1c48646c15 100644 --- a/src/status_im2/contexts/chat/messages/delete_message_for_me/events.cljs +++ b/src/status_im2/contexts/chat/messages/delete_message_for_me/events.cljs @@ -1,8 +1,11 @@ (ns status-im2.contexts.chat.messages.delete-message-for-me.events - (:require [status-im.chat.models.message-list :as message-list] - [utils.datetime :as datetime] - [taoensso.timbre :as log] - [utils.re-frame :as rf])) + (:require + [i18n.i18n :as i18n] + [quo2.foundations.colors :as colors] + [status-im.chat.models.message-list :as message-list] + [taoensso.timbre :as log] + [utils.datetime :as datetime] + [utils.re-frame :as rf])) (defn- update-db-clear-undo-timer [db chat-id message-id] @@ -42,15 +45,37 @@ {: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]) - (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}]))) + (let [existing-undo-toast (get-in db [:toasts :toasts :delete-message-for-me]) + toast-count (inc (get existing-undo-toast :message-deleted-for-me-count 0)) + existing-undos (-> existing-undo-toast + (get :message-deleted-for-me-undos []) + (conj {:message-id message-id :chat-id chat-id}))] + (assoc + (message-list/rebuild-message-list + {:db (reduce + ;; sync all pending deletes' undo timelimit, extend to the latest one + (fn [db-acc {:keys [message-id chat-id]}] + (update-db-delete-locally db-acc chat-id message-id undo-time-limit-ms)) + db + existing-undos)} + chat-id) + :dispatch-n [[:toasts/close :delete-message-for-me] + [:toasts/upsert :delete-message-for-me + {:icon :info + :icon-color colors/danger-50-opa-40 + :message-deleted-for-me-count toast-count + :message-deleted-for-me-undos existing-undos + :text (i18n/label-pluralize toast-count + :t/message-deleted-for-you-count) + :duration undo-time-limit-ms + :undo-duration (/ undo-time-limit-ms 1000) + :undo-on-press #(do (rf/dispatch + [:chat.ui/undo-all-delete-message-for-me]) + (rf/dispatch [:toasts/close + :delete-message-for-me]))}]] + :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}])))) (rf/defn undo {:events [:chat.ui/undo-delete-message-for-me]} @@ -60,17 +85,37 @@ {:db (update-db-undo-locally db chat-id message-id)} chat-id))) +(rf/defn undo-all + {:events [:chat.ui/undo-all-delete-message-for-me]} + [{:keys [db]}] + (when-let [pending-undos (get-in db + [:toasts :toasts :delete-message-for-me + :message-deleted-for-me-undos])] + {:dispatch-n (mapv #(vector :chat.ui/undo-delete-message-for-me %) pending-undos)})) + +(defn- check-before-delete-and-sync + "Make sure message alredy deleted-for-me? locally and undo timelimit has passed" + [db chat-id message-id] + (let [message (get-in db [:messages chat-id message-id]) + {:keys [deleted-for-me? deleted-for-me-undoable-till]} message] + (and deleted-for-me? + deleted-for-me-undoable-till + (>= (datetime/timestamp) deleted-for-me-undoable-till)))) + (rf/defn delete-and-sync {:events [:chat.ui/delete-message-for-me-and-sync]} - [{:keys [db]} {:keys [message-id chat-id]}] - (when (get-in db [:messages chat-id message-id]) - {:db (update-db-clear-undo-timer db chat-id message-id) - :json-rpc/call [{:method "wakuext_deleteMessageForMeAndSync" - :params [chat-id message-id] - :js-response true - :on-error #(log/error "failed to delete message for me, message id: " - {:message-id message-id :error %}) - :on-success #(rf/dispatch [:sanitize-messages-and-process-response %])}]})) + [{:keys [db]} {:keys [message-id chat-id]} force?] + (when-let [message (get-in db [:messages chat-id message-id])] + (when (or force? (check-before-delete-and-sync db chat-id message-id)) + {:db (update-db-clear-undo-timer db chat-id message-id) + :json-rpc/call [{:method "wakuext_deleteMessageForMeAndSync" + :params [chat-id message-id] + :js-response true + :on-error #(log/error + "failed to delete message for me, message id: " + {:message-id message-id :error %}) + :on-success #(rf/dispatch [:sanitize-messages-and-process-response + %])}]}))) (defn- filter-pending-sync-messages "traverse all messages find not yet synced deleted-for-me? messages" @@ -86,4 +131,4 @@ {:events [:chat.ui/sync-all-deleted-for-me-messages]} [{:keys [db] :as cofx}] (let [pending-sync-messages (reduce-kv filter-pending-sync-messages [] (:messages db))] - (apply rf/merge cofx (map delete-and-sync pending-sync-messages)))) + (apply rf/merge cofx (map #(delete-and-sync % true) pending-sync-messages)))) diff --git a/src/status_im2/contexts/chat/messages/delete_message_for_me/events_test.cljs b/src/status_im2/contexts/chat/messages/delete_message_for_me/events_test.cljs index ec6491f23d..9f5928331f 100644 --- a/src/status_im2/contexts/chat/messages/delete_message_for_me/events_test.cljs +++ b/src/status_im2/contexts/chat/messages/delete_message_for_me/events_test.cljs @@ -18,6 +18,43 @@ (is (= (:id result-message) mid)) (is (true? (:deleted-for-me? result-message))) (is (= (:deleted-for-me-undoable-till result-message) 1001)))) + (testing "delete with pending deletes" + (let [db (-> db + (update-in [:messages cid "pending-delete-message"] + assoc + :deleted-for-me? true + :deleted-for-me-undoable-till 0 + :whisper-timestamp 0) + (update-in [:toasts :toasts :delete-message-for-me] + assoc + :message-deleted-for-me-count 1 + :message-deleted-for-me-undos [{:message-id + "pending-delete-message" + :chat-id cid}])) + effects (delete-message-for-me/delete {:db db} message 1000)] + (is (= (get-in effects [:db :messages cid mid :deleted-for-me-undoable-till]) + (get-in effects + [:db :messages cid "pending-delete-message" :deleted-for-me-undoable-till]) + 1001) + "sync all pending delete undo timelimit") + (let [upsert-toast (-> effects :dispatch-n second)] + (is (= (-> upsert-toast last :message-deleted-for-me-count) 2) + "+1 pending deletes") + (is + (and + (-> upsert-toast + last + :message-deleted-for-me-undos + first + :message-id + (= "pending-delete-message")) + (-> upsert-toast + last + :message-deleted-for-me-undos + second + :message-id + (= mid))) + "pending deletes are in order")))) (testing "return nil if message not in db" (is (= (delete-message-for-me/delete {:db {:messages []}} message 1000) nil))))))) @@ -56,46 +93,42 @@ nil)))))) (deftest delete-for-me-and-sync - (let [db {:messages {cid {mid {:id mid}}}} + (let [db {:messages {cid {mid {:id mid :deleted-for-me? true :deleted-for-me-undoable-till 0}}}} message {:message-id mid :chat-id cid}] (testing "delete for me and sync" (testing "dispatch right rpc call" - (let [expected-db {:messages {cid {mid {:id mid}}}} - effects (delete-message-for-me/delete-and-sync {:db db} message) + (let [expected-db {:messages {cid {mid {:id mid :deleted-for-me? true}}}} + effects (delete-message-for-me/delete-and-sync {:db db} message false) result-db (:db effects) rpc-calls (:json-rpc/call effects)] (is (= result-db expected-db)) (is (= (count rpc-calls) 1)) - (is (= (-> rpc-calls - first - :method) - "wakuext_deleteMessageForMeAndSync")) - (is (= (-> rpc-calls - first - :params - count) - 2)) - (is (= (-> rpc-calls - first - :params - first) - cid)) - (is (= (-> rpc-calls - first - :params - second) - mid)))) + (is (= (-> rpc-calls first :method) "wakuext_deleteMessageForMeAndSync")) + (is (= (-> rpc-calls first :params count) 2)) + (is (= (-> rpc-calls first :params first) cid)) + (is (= (-> rpc-calls first :params second) mid)))) (testing "clean undo timer" - (let [expected-db {:messages {cid {mid {:id mid}}}} + (let [expected-db {:messages {cid {mid {:id mid :deleted-for-me? true}}}} effects (delete-message-for-me/delete-and-sync {:db (update-in db - [:messages cid mid - :deleted-for-me-undoable-till] + [:messages cid mid :deleted-for-me-undoable-till] (constantly (datetime/timestamp)))} - message) + message + false) result-db (:db effects)] (is (= result-db expected-db)))) + (testing "before deleted locally" + (let [effects (delete-message-for-me/delete-and-sync + {:db (update-in db [:messages cid mid] dissoc :deleted-for-me?)} + message + false)] + (is (-> effects :db nil?) "not delete and send"))) + (testing "before undo timelimit" + (with-redefs [datetime/timestamp (constantly 1)] + (let [effects (delete-message-for-me/delete-and-sync + {:db (update-in db [:messages cid mid] assoc :deleted-for-me-undoable-till 2)} + message + false)] + (is (-> effects :db nil?))))) (testing "return nil if message not in db" - (is (= (delete-message-for-me/delete-and-sync {:db {:messages []}} - message) - nil)))))) + (is (= (delete-message-for-me/delete-and-sync {:db {:messages []}} message false) nil)))))) diff --git a/src/status_im2/setup/db.cljs b/src/status_im2/setup/db.cljs index 0a9fbd2458..5780c37a2b 100644 --- a/src/status_im2/setup/db.cljs +++ b/src/status_im2/setup/db.cljs @@ -34,6 +34,7 @@ :chat/spam-messages-frequency 0 :chats-home-list #{} :home-items-show-number 20 + :toasts {:ordered '() :toasts {}} :tooltips {} :dimensions/window (rn/get-window) :registry {} diff --git a/src/status_im2/subs/toasts.cljs b/src/status_im2/subs/toasts.cljs index 98b38ec71b..d4a4795fce 100644 --- a/src/status_im2/subs/toasts.cljs +++ b/src/status_im2/subs/toasts.cljs @@ -6,3 +6,9 @@ :<- [:toasts] (fn [toasts [_ toast-id]] (get-in toasts [:toasts toast-id]))) + +(re-frame/reg-sub + :toasts/toast-cursor + :<- [:toasts] + (fn [toasts [_ toast-id & cursor]] + (get-in toasts (into [:toasts toast-id] cursor)))) diff --git a/translations/en.json b/translations/en.json index d58fe11d30..566b57826f 100644 --- a/translations/en.json +++ b/translations/en.json @@ -878,7 +878,15 @@ "message": "Message", "message-deleted": "Message deleted", "message-deleted-for-everyone": "Message deleted for everyone", + "message-deleted-for-everyone-count": { + "one": "1 message deleted for everyone", + "other": "{{count}} messages deleted for everyone" + }, "message-deleted-for-you": "Message deleted for you", + "message-deleted-for-you-count": { + "one": "1 message deleted for you", + "other": "{{count}} messages deleted for you" + }, "message-not-sent": "Message not sent", "message-options-cancel": "Cancel", "message-reply": "Reply",