diff --git a/doc/new-guidelines.md b/doc/new-guidelines.md index ae126d63a5..437999be1c 100644 --- a/doc/new-guidelines.md +++ b/doc/new-guidelines.md @@ -365,6 +365,43 @@ dispatch. (str "Hello " username)]]) ``` +### Registering effects + +When registering re-frame effects (`reg-fx`), prefer to expose a data-only +interface because that will allow event handlers to stay pure. + +For instance, if an effect needs a `on-success` callback, allow it to receive a +*re-frame event vector*. This approach is used by us in the [json-rpc/call +effect](src/status_im2/common/json_rpc/events.cljs), but also by third-party +effects, such as https://github.com/Day8/re-frame-http-fx. For the complete +rationale, see [PR #15936](https://github.com/status-im/status-mobile/pull/15936). + +### Using the effect `:json-rpc/call` + +Prefer the pure version of `:json-rpc/call` (no callbacks). + +```clojure +;; not as good +(rf/defn accept-contact-request + {:events [:activity-center.contact-requests/accept]} + [_ contact-id] + {:json-rpc/call + [{:method "wakuext_acceptContactRequest" + :params [{:id contact-id}] + :on-success #(rf/dispatch [:sanitize-messages-and-process-response %]) + :on-error #(rf/dispatch [:activity-center.contact-requests/accept-error contact-id %])}]}) + +;; better +(rf/defn accept-contact-request + {:events [:activity-center.contact-requests/accept]} + [_ contact-id] + {:json-rpc/call + [{:method "wakuext_acceptContactRequest" + :params [{:id contact-id}] + :on-success [:sanitize-messages-and-process-response] + :on-error [:activity-center.contact-requests/accept-error contact-id]}]}) +``` + ### Registering event handlers Events must always be declared with the `utils.fx/defn` macro. Also, don't use diff --git a/src/status_im2/common/json_rpc/events.cljs b/src/status_im2/common/json_rpc/events.cljs index 776da8ad84..592ad555a5 100644 --- a/src/status_im2/common/json_rpc/events.cljs +++ b/src/status_im2/common/json_rpc/events.cljs @@ -1,12 +1,13 @@ (ns status-im2.common.json-rpc.events (:require [clojure.string :as string] + [native-module.core :as native-module] [re-frame.core :as re-frame] [react-native.background-timer :as background-timer] - [native-module.core :as native-module] [taoensso.timbre :as log] + [utils.re-frame :as rf] [utils.transforms :as transforms])) -(defn on-error-retry +(defn- on-error-retry [call-method {:keys [method number-of-retries delay on-error] :as arg}] (if (pos? number-of-retries) (fn [error] @@ -24,6 +25,31 @@ on-error)) (defn call + "Call private RPC endpoint. + + method: string - The name of an endpoint function in status-go, with the first + character lowercased and prefixed by wakuext_. For example, the BackupData + function should be represented as the string wakuext_backupData. + + params: sequence - A positional sequence of zero or more arguments. + + on-success/on-error: function/vector (optional) - When a function, it will be + called with the transformed response as the sole argument. When a vector, it + is expected to be a valid re-frame event vector, and the event will be + dispatched with the transformed response conj'ed at the end. + + js-response: boolean - When non-nil, the successful response will not be + recursively converted to Clojure data structures. Default: nil. + + number-of-retries: integer - The maximum number of retries in case of failure. + Default: nil. + + delay: integer - The number of milliseconds to wait between retries. Default: + nil. + + Note that on-error is optional, but if not provided, a default implementation + will be used. + " [{:keys [method params on-success on-error js-response] :as arg}] (let [params (or params []) on-error (or on-error @@ -34,15 +60,25 @@ :id 1 :method method :params params}) - (fn [response] - (if (string/blank? response) - (on-error {:message "Blank response"}) - (let [response-js (transforms/json->js response)] - (if (.-error response-js) - (on-error (transforms/js->clj (.-error response-js))) - (on-success (if js-response - (.-result response-js) - (transforms/js->clj (.-result response-js))))))))))) + (fn [raw-response] + (if (string/blank? raw-response) + (let [error {:message "Blank response"}] + (if (vector? on-error) + (rf/dispatch (conj on-error error)) + (on-error error))) + (let [^js response-js (transforms/json->js raw-response)] + (if-let [error (.-error response-js)] + (let [error (transforms/js->clj error)] + (if (vector? on-error) + (rf/dispatch (conj on-error error)) + (on-error error))) + (when on-success + (let [result (if js-response + (.-result response-js) + (transforms/js->clj (.-result response-js)))] + (if (vector? on-success) + (rf/dispatch (conj on-success result)) + (on-success result))))))))))) (re-frame/reg-fx :json-rpc/call diff --git a/src/status_im2/contexts/activity_center/events.cljs b/src/status_im2/contexts/activity_center/events.cljs index 7e2af8d33e..bfabe62060 100644 --- a/src/status_im2/contexts/activity_center/events.cljs +++ b/src/status_im2/contexts/activity_center/events.cljs @@ -112,12 +112,9 @@ (when-let [notification (get-notification db notification-id)] {:json-rpc/call [{:method "wakuext_markActivityCenterNotificationsRead" :params [[notification-id]] - :on-success #(rf/dispatch [:activity-center.notifications/mark-as-read-success - notification]) - :on-error #(rf/dispatch [:activity-center/process-notification-failure - notification-id - :notification/mark-as-read - %])}]})) + :on-success [:activity-center.notifications/mark-as-read-success notification] + :on-error [:activity-center/process-notification-failure notification-id + :notification/mark-as-read]}]})) (rf/defn mark-as-read-success {:events [:activity-center.notifications/mark-as-read-success]} @@ -130,12 +127,9 @@ (when-let [notification (get-notification db notification-id)] {:json-rpc/call [{:method "wakuext_markActivityCenterNotificationsUnread" :params [[notification-id]] - :on-success #(rf/dispatch [:activity-center.notifications/mark-as-unread-success - notification]) - :on-error #(rf/dispatch [:activity-center/process-notification-failure - notification-id - :notification/mark-as-unread - %])}]})) + :on-success [:activity-center.notifications/mark-as-unread-success notification] + :on-error [:activity-center/process-notification-failure notification-id + :notification/mark-as-unread]}]})) (rf/defn mark-as-unread-success {:events [:activity-center.notifications/mark-as-unread-success]} @@ -149,12 +143,9 @@ (when (>= now undoable-till) {:json-rpc/call [{:method "wakuext_markAllActivityCenterNotificationsRead" :params [] - :on-success #(rf/dispatch - [:activity-center.notifications/mark-all-as-read-success]) - :on-error #(rf/dispatch [:activity-center/process-notification-failure - nil - :notification/mark-all-as-read - %])}]}))) + :on-success [:activity-center.notifications/mark-all-as-read-success] + :on-error [:activity-center/process-notification-failure nil + :notification/mark-all-as-read]}]}))) (rf/defn mark-all-as-read-success {:events [:activity-center.notifications/mark-all-as-read-success]} @@ -210,17 +201,14 @@ [{:keys [db]} notification-id] {:json-rpc/call [{:method "wakuext_acceptActivityCenterNotifications" :params [[notification-id]] - :on-success #(rf/dispatch [:activity-center.notifications/accept-success - notification-id %]) - :on-error #(rf/dispatch [:activity-center/process-notification-failure - notification-id - :notification/accept - %])}]}) + :on-success [:activity-center.notifications/accept-success notification-id] + :on-error [:activity-center/process-notification-failure notification-id + :notification/accept]}]}) (rf/defn accept-notification-success {:events [:activity-center.notifications/accept-success]} [{:keys [db] :as cofx} notification-id {:keys [chats]}] - (let [notification (get-notification db notification-id)] + (when-let [notification (get-notification db notification-id)] (rf/merge cofx (chat.events/ensure-chats (map data-store.chats/<-rpc chats)) (notifications-reconcile [(assoc notification :read true :accepted true)])))) @@ -230,17 +218,14 @@ [{:keys [db]} notification-id] {:json-rpc/call [{:method "wakuext_dismissActivityCenterNotifications" :params [[notification-id]] - :on-success #(rf/dispatch [:activity-center.notifications/dismiss-success - notification-id]) - :on-error #(rf/dispatch [:activity-center/process-notification-failure - notification-id - :notification/dismiss - %])}]}) + :on-success [:activity-center.notifications/dismiss-success notification-id] + :on-error [:activity-center/process-notification-failure notification-id + :notification/dismiss]}]}) (rf/defn dismiss-notification-success {:events [:activity-center.notifications/dismiss-success]} [{:keys [db] :as cofx} notification-id] - (let [notification (get-notification db notification-id)] + (when-let [notification (get-notification db notification-id)] (notifications-reconcile cofx [(assoc notification :read true :dismissed true)]))) (rf/defn delete-notification @@ -248,12 +233,9 @@ [{:keys [db]} notification-id] {:json-rpc/call [{:method "wakuext_deleteActivityCenterNotifications" :params [[notification-id]] - :on-success #(rf/dispatch [:activity-center.notifications/delete-success - notification-id]) - :on-error #(rf/dispatch [:activity-center/process-notification-failure - notification-id - :notification/delete - %])}]}) + :on-success [:activity-center.notifications/delete-success notification-id] + :on-error [:activity-center/process-notification-failure notification-id + :notification/delete]}]}) (rf/defn delete-notification-success {:events [:activity-center.notifications/delete-success]} @@ -268,48 +250,36 @@ [_ notification-id] {:json-rpc/call [{:method "wakuext_declineContactVerificationRequest" :params [notification-id] - :on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response - %]) - :on-error #(rf/dispatch [:activity-center/process-notification-failure - notification-id - :contact-verification/decline - %])}]}) + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/decline]}]}) (rf/defn contact-verification-reply {:events [:activity-center.contact-verification/reply]} [_ notification-id reply] {:json-rpc/call [{:method "wakuext_acceptContactVerificationRequest" :params [notification-id reply] - :on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response - %]) - :on-error #(rf/dispatch [:activity-center/process-notification-failure - notification-id - :contact-verification/reply - %])}]}) + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/reply]}]}) (rf/defn contact-verification-mark-as-trusted {:events [:activity-center.contact-verification/mark-as-trusted]} [_ notification-id] {:json-rpc/call [{:method "wakuext_verifiedTrusted" :params [{:id notification-id}] - :on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response - %]) - :on-error #(rf/dispatch [:activity-center/process-notification-failure - notification-id - :contact-verification/mark-as-trusted - %])}]}) + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/mark-as-trusted]}]}) (rf/defn contact-verification-mark-as-untrustworthy {:events [:activity-center.contact-verification/mark-as-untrustworthy]} [_ notification-id] {:json-rpc/call [{:method "wakuext_verifiedUntrustworthy" :params [{:id notification-id}] - :on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response - %]) - :on-error #(rf/dispatch [:activity-center/process-notification-failure - notification-id - :contact-verification/mark-as-untrustworthy - %])}]}) + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/mark-as-untrustworthy]}]}) ;;;; Notifications fetching and pagination @@ -368,10 +338,9 @@ :limit per-page :activityTypes (filter-type->rpc-param filter-type) :readType (->rpc-read-type filter-status)}] - :on-success #(rf/dispatch [:activity-center.notifications/fetch-success - reset-data? %]) - :on-error #(rf/dispatch [:activity-center.notifications/fetch-error - filter-type filter-status %])}]}))) + :on-success [:activity-center.notifications/fetch-success reset-data?] + :on-error [:activity-center.notifications/fetch-error filter-type + filter-status]}]}))) (rf/defn notifications-fetch-first-page {:events [:activity-center.notifications/fetch-first-page]} @@ -434,9 +403,8 @@ :limit 20 :activityTypes [types/contact-request] :readType (->rpc-read-type :unread)}] - :on-success #(rf/dispatch [:activity-center.notifications/fetch-pending-contact-requests-success %]) - :on-error #(rf/dispatch [:activity-center.notifications/fetch-error types/contact-request :unread - %])}]}) + :on-success [:activity-center.notifications/fetch-pending-contact-requests-success] + :on-error [:activity-center.notifications/fetch-error types/contact-request :unread]}]}) (rf/defn notifications-fetch-pending-contact-requests-success {:events [:activity-center.notifications/fetch-pending-contact-requests-success]} @@ -464,8 +432,8 @@ {:json-rpc/call [{:method "wakuext_hasUnseenActivityCenterNotifications" :params [] - :on-success #(rf/dispatch [:activity-center/update-seen-state-success %]) - :on-error #(rf/dispatch [:activity-center/update-seen-state-error %])}]}) + :on-success [:activity-center/update-seen-state-success] + :on-error [:activity-center/update-seen-state-error]}]}) (rf/defn update-seen-state-success {:events [:activity-center/update-seen-state-success]} @@ -485,8 +453,8 @@ {:json-rpc/call [{:method "wakuext_markAsSeenActivityCenterNotifications" :params [] - :on-success #(rf/dispatch [:activity-center/mark-as-seen-success %]) - :on-error #(rf/dispatch [:activity-center/mark-as-seen-error %])}]}) + :on-success [:activity-center/mark-as-seen-success] + :on-error [:activity-center/mark-as-seen-error]}]}) (rf/defn mark-as-seen-success {:events [:activity-center/mark-as-seen-success]} @@ -509,8 +477,8 @@ [{:method "wakuext_activityCenterNotificationsCount" :params [{:activityTypes types/all-supported :readType (->rpc-read-type :unread)}] - :on-success #(rf/dispatch [:activity-center.notifications/fetch-unread-count-success %]) - :on-error #(rf/dispatch [:activity-center.notifications/fetch-unread-count-error %])}]}) + :on-success [:activity-center.notifications/fetch-unread-count-success] + :on-error [:activity-center.notifications/fetch-unread-count-error]}]}) (rf/defn notifications-fetch-unread-count-success {:events [:activity-center.notifications/fetch-unread-count-success]} diff --git a/src/status_im2/contexts/activity_center/events_test.cljs b/src/status_im2/contexts/activity_center/events_test.cljs index 698893d007..33059c9006 100644 --- a/src/status_im2/contexts/activity_center/events_test.cljs +++ b/src/status_im2/contexts/activity_center/events_test.cljs @@ -1,163 +1,169 @@ (ns status-im2.contexts.activity-center.events-test (:require [cljs.test :refer [deftest is testing]] [status-im2.constants :as constants] - status-im.events - [test-helpers.unit :as h] + [status-im2.contexts.activity-center.events :as events] [status-im2.contexts.activity-center.notification-types :as types] - [utils.re-frame :as rf])) + [test-helpers.unit :as h])) (h/use-log-fixture) (def notification-id "0x1") -(defn setup - [] - (h/register-helper-events) - (rf/dispatch [:setup/app-started])) - -(defn test-log-on-failure - [{:keys [before-test notification-id event action]}] - (h/run-test-sync - (setup) - (when before-test - (before-test)) - (h/stub-fx-with-callbacks :json-rpc/call :on-error (constantly :fake-error)) - - (rf/dispatch event) - - (is (= {:args [(str "Failed to " action) - {:notification-id notification-id - :error :fake-error}] - :level :warn} - (last @h/logs))))) - ;;;; Misc (deftest open-activity-center-test - (testing "opens the activity center with filters enabled" - (h/run-test-sync - (setup) - (rf/dispatch [:activity-center/open - {:filter-type types/contact-request - :filter-status :unread}]) - - (is (= {:status :unread - :type types/contact-request} - (get-in (h/db) [:activity-center :filter]))))) - (testing "opens the activity center with default filters" - (h/run-test-sync - (setup) + (is (= {:db {} + :dispatch [:open-modal :activity-center {}] + :dispatch-later [{:ms 1000 :dispatch [:activity-center/mark-as-seen]}]} + (events/open-activity-center {:db {}} nil)))) - (rf/dispatch [:activity-center/open]) + (testing "opens the activity center with filters enabled" + (is (= {:db {:activity-center {:filter {:status :unread :type types/contact-request}}} + :dispatch [:open-modal :activity-center {}] + :dispatch-later [{:ms 1000 :dispatch [:activity-center/mark-as-seen]}]} + (events/open-activity-center {:db {}} + {:filter-type types/contact-request + :filter-status :unread}))))) - (is (= {:status :unread :type types/no-type} - (get-in (h/db) [:activity-center :filter])))))) +(deftest process-notification-failure-test + (testing "logs and returns nil" + (is (nil? (events/process-notification-failure + {:db {}} + notification-id + :some-action-name + :some-error))) + (is (= {:args ["Failed to :some-action-name" + {:notification-id notification-id + :error :some-error}] + :level :warn} + (last @h/logs))))) + +;;;; Mark as read/unread (deftest mark-as-read-test (testing "does nothing if the notification ID cannot be found in the app db" - (h/run-test-sync - (setup) - (let [spy-queue (atom [])] - (h/spy-fx spy-queue :json-rpc/call) - (let [notifications [{:id notification-id - :read false - :type types/one-to-one-chat}]] - (rf/dispatch [:test/assoc-in [:activity-center :notifications] notifications]) + (let [cofx {:db {:activity-center + {:notifications [{:id "0x1" + :read false + :type types/one-to-one-chat}]}}}] + (is (nil? (events/mark-as-read cofx "0x99"))))) - (rf/dispatch [:activity-center.notifications/mark-as-read "0x666"]) + (testing "dispatches RPC call" + (let [notif {:id "0x1" :read false :type types/one-to-one-chat} + cofx {:db {:activity-center {:notifications [notif]}}}] + (is (= {:json-rpc/call + [{:method "wakuext_markActivityCenterNotificationsRead" + :params [[(:id notif)]] + :on-success [:activity-center.notifications/mark-as-read-success notif] + :on-error [:activity-center/process-notification-failure (:id notif) + :notification/mark-as-read]}]} + (events/mark-as-read cofx (:id notif))))))) - (is (= [] @spy-queue)) - (is (= notifications (get-in (h/db) [:activity-center :notifications]))))))) +(deftest mark-as-read-success-test + (let [f-args (atom []) + cofx {:db {}} + notif {:id "0x1" :read false :type types/one-to-one-chat}] + (with-redefs [events/notifications-reconcile + (fn [& args] + (reset! f-args args) + :result)] + (is (= :result (events/mark-as-read-success cofx notif))) + (is (= [cofx [(assoc notif :read true)]] + @f-args))))) - (testing "marks notifications as read and updates app db" - (h/run-test-sync - (setup) - (let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat} - notif-2 {:id "0x2" :read false :type types/one-to-one-chat} - notif-3 {:id "0x3" :read false :type types/one-to-one-chat} - new-notif-3 (assoc notif-3 :read true) - new-notif-2 (assoc notif-2 :read true)] - (h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly nil)) - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:status :all :type types/no-type} - :notifications [notif-3 notif-2 notif-1]}]) +(deftest mark-as-unread-test + (testing "does nothing if the notification ID cannot be found in the app db" + (let [cofx {:db {:activity-center + {:notifications [{:id "0x1" + :read true + :type types/one-to-one-chat}]}}}] + (is (nil? (events/mark-as-unread cofx "0x99"))))) - (rf/dispatch [:activity-center.notifications/mark-as-read (:id notif-2)]) - (is (= [notif-3 new-notif-2 notif-1] - (get-in (h/db) [:activity-center :notifications]))) + (testing "dispatches RPC call" + (let [notif {:id "0x1" :read true :type types/one-to-one-chat} + cofx {:db {:activity-center {:notifications [notif]}}}] + (is (= {:json-rpc/call + [{:method "wakuext_markActivityCenterNotificationsUnread" + :params [[(:id notif)]] + :on-success [:activity-center.notifications/mark-as-unread-success notif] + :on-error [:activity-center/process-notification-failure (:id notif) + :notification/mark-as-unread]}]} + (events/mark-as-unread cofx (:id notif))))))) - (rf/dispatch [:activity-center.notifications/mark-as-read (:id notif-3)]) - (is (= [new-notif-3 new-notif-2 notif-1] - (get-in (h/db) [:activity-center :notifications])))))) - - (testing "logs on failure" - (test-log-on-failure - {:notification-id notification-id - :event [:activity-center.notifications/mark-as-read notification-id] - :action :notification/mark-as-read - :before-test (fn [] - (rf/dispatch - [:test/assoc-in [:activity-center :notifications] - [{:id notification-id - :read false - :type types/one-to-one-chat}]]))}))) +(deftest mark-as-unread-success-test + (let [f-args (atom []) + cofx {:db {}} + notif {:id "0x1" :read true :type types/one-to-one-chat}] + (with-redefs [events/notifications-reconcile + (fn [& args] + (reset! f-args args) + :reconciliation-result)] + (is (= :reconciliation-result (events/mark-as-unread-success cofx notif))) + (is (= [cofx [(assoc notif :read false)]] + @f-args))))) ;;;; Acceptance/dismissal -(deftest notification-acceptance-test +(deftest accept-notification-test + (is (= {:json-rpc/call + [{:method "wakuext_acceptActivityCenterNotifications" + :params [[notification-id]] + :on-success [:activity-center.notifications/accept-success notification-id] + :on-error [:activity-center/process-notification-failure notification-id + :notification/accept]}]} + (events/accept-notification {:db {}} notification-id)))) + +(deftest accept-notification-success-test + (testing "does nothing if the notification ID cannot be found in the app db" + (let [cofx {:db {:activity-center + {:notifications [{:id "0x1" + :read false + :type types/one-to-one-chat}]}}}] + (is (nil? (events/accept-notification-success cofx "0x99" nil))))) + (testing "marks notification as accepted and read, then reconciles" - (h/run-test-sync - (setup) - (let [notif-1 {:id "0x1" :type types/private-group-chat} - notif-2 {:id "0x2" :type types/private-group-chat} - notif-2-accepted (assoc notif-2 :accepted true :read true)] - (h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly notif-2)) - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:type types/no-type :status :all} - :notifications [notif-2 notif-1]}]) + (let [notif-1 {:id "0x1" :type types/private-group-chat} + notif-2 {:id "0x2" :type types/private-group-chat} + notif-2-accepted (assoc notif-2 :accepted true :read true) + cofx {:db {:activity-center {:filter {:type types/no-type :status :all} + :notifications [notif-2 notif-1]}}}] + (is (= {:db {:activity-center {:filter {:type 0 :status :all} + :notifications [notif-2-accepted notif-1]} + :chats {} + :chats-home-list nil} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/accept-notification-success cofx (:id notif-2) nil)))))) - (rf/dispatch [:activity-center.notifications/accept (:id notif-2)]) +(deftest dismiss-notification-test + (is (= {:json-rpc/call + [{:method "wakuext_dismissActivityCenterNotifications" + :params [[notification-id]] + :on-success [:activity-center.notifications/dismiss-success notification-id] + :on-error [:activity-center/process-notification-failure notification-id + :notification/dismiss]}]} + (events/dismiss-notification {:db {}} notification-id)))) - (is (= [notif-2-accepted notif-1] - (get-in (h/db) [:activity-center :notifications]))) +(deftest dismiss-notification-success-test + (testing "does nothing if the notification ID cannot be found in the app db" + (let [cofx {:db {:activity-center + {:notifications [{:id "0x1" + :read false + :type types/one-to-one-chat}]}}}] + (is (nil? (events/dismiss-notification-success cofx "0x99"))))) - ;; Ignores accepted notification if the Unread filter is enabled because - ;; accepted notifications are also marked as read in status-go. - (rf/dispatch [:test/assoc-in [:activity-center :filter] - {:filter {:type types/no-type :status :unread}}]) - (rf/dispatch [:activity-center.notifications/accept (:id notif-2)]) - (is (= [notif-1] - (get-in (h/db) [:activity-center :notifications])))))) - - (testing "logs on failure" - (test-log-on-failure - {:notification-id notification-id - :event [:activity-center.notifications/accept notification-id] - :action :notification/accept}))) - -(deftest notification-dismissal-test - (testing "dismisses & mark notification as read, and keep it in the app db" - (h/run-test-sync - (setup) - (let [notif-1 {:id "0x1" :type types/private-group-chat} - notif-2 {:id "0x2" :type types/admin} - dismissed-notif-1 (assoc notif-1 :dismissed true :read true)] - (h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly notif-2)) - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:type types/no-type :status :all} - :notifications [notif-2 notif-1]}]) - - (rf/dispatch [:activity-center.notifications/dismiss (:id notif-1)]) - - (is (= [notif-2 dismissed-notif-1] - (get-in (h/db) [:activity-center :notifications])))))) - - (testing "logs on failure" - (test-log-on-failure - {:notification-id notification-id - :event [:activity-center.notifications/dismiss notification-id] - :action :notification/dismiss}))) + (testing "marks notification as dismissed and read, then reconciles" + (let [notif-1 {:id "0x1" :type types/private-group-chat} + notif-2 {:id "0x2" :type types/private-group-chat} + notif-2-dismissed (assoc notif-2 :dismissed true :read true) + cofx {:db {:activity-center {:filter {:type types/no-type :status :all} + :notifications [notif-2 notif-1]}}}] + (is (= {:db {:activity-center {:filter {:type 0 :status :all} + :notifications [notif-2-dismissed notif-1]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/dismiss-notification-success cofx (:id notif-2))))))) ;;;; Contact verification @@ -204,339 +210,228 @@ :type types/contact-verification}) (deftest contact-verification-decline-test - (testing "declines notification and reconciles" - (h/run-test-sync - (setup) - (let [spy-queue (atom [])] - (h/stub-fx-with-callbacks :json-rpc/call - :on-success - (constantly contact-verification-rpc-response)) - (h/spy-fx spy-queue :json-rpc/call) - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:type types/contact-verification :status :all}}]) - - (rf/dispatch [:activity-center.contact-verification/decline notification-id]) - - (is (= [contact-verification-expected-notification] - (get-in (h/db) [:activity-center :notifications])))))) - - (testing "logs on failure" - (test-log-on-failure - {:notification-id notification-id - :event [:activity-center.contact-verification/decline notification-id] - :action :contact-verification/decline}))) + (is (= {:json-rpc/call + [{:method "wakuext_declineContactVerificationRequest" + :params [notification-id] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/decline]}]} + (events/contact-verification-decline {:db {}} notification-id)))) (deftest contact-verification-reply-test - (testing "sends reply and reconciles" - (let [reply "any answer"] - (h/run-test-sync - (setup) - (let [spy-queue (atom [])] - (h/stub-fx-with-callbacks :json-rpc/call - :on-success - (constantly contact-verification-rpc-response)) - (h/spy-fx spy-queue :json-rpc/call) - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:type types/contact-verification :status :all}}]) - - (rf/dispatch [:activity-center.contact-verification/reply notification-id reply]) - - (is (= [contact-verification-expected-notification] - (get-in (h/db) [:activity-center :notifications]))))))) - - (testing "logs on failure" - (test-log-on-failure - {:notification-id notification-id - :event [:activity-center.contact-verification/reply notification-id "any answer"] - :action :contact-verification/reply}))) + (let [reply "The answer is 42"] + (is (= {:json-rpc/call + [{:method "wakuext_acceptContactVerificationRequest" + :params [notification-id reply] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/reply]}]} + (events/contact-verification-reply {:db {}} notification-id reply))))) (deftest contact-verification-mark-as-trusted-test - (testing "app db reconciliation" - (h/run-test-sync - (setup) - (h/stub-fx-with-callbacks :json-rpc/call - :on-success - (constantly contact-verification-rpc-response)) - - ;; With "Unread" filter disabled - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:type types/no-type :status :all}}]) - (rf/dispatch [:activity-center.contact-verification/mark-as-trusted notification-id]) - (is (= [contact-verification-expected-notification] - (get-in (h/db) [:activity-center :notifications]))) - - ;; With "Unread" filter enabled - (rf/dispatch [:test/assoc-in [:activity-center :filter :status] :unread]) - (rf/dispatch [:activity-center.contact-verification/mark-as-trusted notification-id]) - (is (= [] (get-in (h/db) [:activity-center :notifications]))))) - - (testing "logs on failure" - (test-log-on-failure - {:notification-id notification-id - :event [:activity-center.contact-verification/mark-as-trusted notification-id] - :action :contact-verification/mark-as-trusted}))) + (is (= {:json-rpc/call + [{:method "wakuext_verifiedTrusted" + :params [{:id notification-id}] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/mark-as-trusted]}]} + (events/contact-verification-mark-as-trusted {:db {}} notification-id)))) (deftest contact-verification-mark-as-untrustworthy-test - (testing "app db reconciliation" - (h/run-test-sync - (setup) - (h/stub-fx-with-callbacks - :json-rpc/call - :on-success - (constantly contact-verification-rpc-response)) - - ;; With "Unread" filter disabled - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:type types/no-type :status :all}}]) - (rf/dispatch [:activity-center.contact-verification/mark-as-untrustworthy notification-id]) - (is (= [contact-verification-expected-notification] - (get-in (h/db) [:activity-center :notifications]))) - - ;; With "Unread" filter enabled - (rf/dispatch [:test/assoc-in [:activity-center :filter :status] :unread]) - (rf/dispatch [:activity-center.contact-verification/mark-as-untrustworthy notification-id]) - (is (= [] (get-in (h/db) [:activity-center :notifications]))))) - - (testing "logs on failure" - (test-log-on-failure - {:notification-id notification-id - :event [:activity-center.contact-verification/mark-as-untrustworthy notification-id] - :action :contact-verification/mark-as-untrustworthy}))) + (is (= {:json-rpc/call + [{:method "wakuext_verifiedUntrustworthy" + :params [{:id notification-id}] + :on-success [:activity-center/reconcile-notifications-from-response] + :on-error [:activity-center/process-notification-failure notification-id + :contact-verification/mark-as-untrustworthy]}]} + (events/contact-verification-mark-as-untrustworthy {:db {}} notification-id)))) ;;;; Notification reconciliation (deftest notifications-reconcile-test (testing "All tab + All filter" - (h/run-test-sync - (setup) - (let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat} - notif-2 {:id "0x2" :read false :type types/system} - new-notif-3 {:id "0x3" :read false :type types/system} - new-notif-4 {:id "0x4" :read true :type types/system} - new-notif-2 (assoc notif-2 :read true)] - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:type types/no-type :status :all} - :notifications [notif-2 notif-1]}]) - - (rf/dispatch - [:activity-center.notifications/reconcile - [(assoc notif-1 :deleted true) ; will be removed - new-notif-2 - new-notif-3 - new-notif-4]]) - - (is (= [new-notif-4 new-notif-3 new-notif-2] - (get-in (h/db) [:activity-center :notifications])))))) + (let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat} + notif-2 {:id "0x2" :read false :type types/system} + new-notif-3 {:id "0x3" :read false :type types/system} + new-notif-4 {:id "0x4" :read true :type types/system} + new-notif-2 (assoc notif-2 :read true) + cofx {:db {:activity-center + {:filter {:type types/no-type :status :all} + :notifications [notif-2 notif-1]}}}] + (is (= {:db {:activity-center + {:filter {:type types/no-type :status :all} + :notifications [new-notif-4 new-notif-3 new-notif-2]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/notifications-reconcile + cofx + [(assoc notif-1 :deleted true) ; will be removed + new-notif-2 + new-notif-3 + new-notif-4]))))) (testing "All tab + Unread filter" - (h/run-test-sync - (setup) - (let [notif-1 {:id "0x1" :read false :type types/one-to-one-chat} - notif-2 {:id "0x2" :read false :type types/system} - new-notif-2 (assoc notif-2 :read true) - new-notif-3 {:id "0x3" :read false :type types/system} - new-notif-4 {:id "0x4" :read true :type types/system} - notif-5 {:id "0x5" :type types/system}] - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:type types/no-type :status :unread} - :notifications [notif-5 notif-2 notif-1]}]) - - (rf/dispatch - [:activity-center.notifications/reconcile - [new-notif-2 ; will be removed because it's read - new-notif-3 ; will be inserted - new-notif-4 ; will be ignored because it's read - (assoc notif-5 :deleted true) ; will be removed - ]]) - - (is (= [new-notif-3 notif-1] - (get-in (h/db) [:activity-center :notifications])))))) + (let [notif-1 {:id "0x1" :read false :type types/one-to-one-chat} + notif-2 {:id "0x2" :read false :type types/system} + new-notif-2 (assoc notif-2 :read true) + new-notif-3 {:id "0x3" :read false :type types/system} + new-notif-4 {:id "0x4" :read true :type types/system} + notif-5 {:id "0x5" :type types/system} + cofx {:db {:activity-center + {:filter {:type types/no-type :status :unread} + :notifications [notif-5 notif-2 notif-1]}}}] + (is (= {:db {:activity-center + {:filter {:type types/no-type :status :unread} + :notifications [new-notif-3 notif-1]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/notifications-reconcile + cofx + [new-notif-2 ; will be removed because it's read + new-notif-3 ; will be inserted + new-notif-4 ; will be ignored because it's read + (assoc notif-5 :deleted true) ; will be removed + ]))))) (testing "Contact request tab + All filter" - (h/run-test-sync - (setup) - (let [notif-1 {:id "0x1" :read true :type types/contact-request} - notif-2 {:id "0x2" :read false :type types/contact-request} - new-notif-2 (assoc notif-2 :read true) - new-notif-3 {:id "0x3" :read false :type types/contact-request} - new-notif-4 {:id "0x4" :read true :type types/system} - notif-5 {:id "0x5" :read false :type types/contact-request}] - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:type types/contact-request :status :all} - :notifications [notif-5 notif-2 notif-1]}]) - - (rf/dispatch - [:activity-center.notifications/reconcile - [new-notif-2 ; will be updated - new-notif-3 ; will be inserted - new-notif-4 ; will be ignored because it's not a contact request - (assoc notif-5 :deleted true) ; will be removed - ]]) - - (is (= [new-notif-3 new-notif-2 notif-1] - (get-in (h/db) [:activity-center :notifications])))))) + (let [notif-1 {:id "0x1" :read true :type types/contact-request} + notif-2 {:id "0x2" :read false :type types/contact-request} + new-notif-2 (assoc notif-2 :read true) + new-notif-3 {:id "0x3" :read false :type types/contact-request} + new-notif-4 {:id "0x4" :read true :type types/system} + notif-5 {:id "0x5" :read false :type types/contact-request} + cofx {:db {:activity-center + {:filter {:type types/contact-request :status :all} + :notifications [notif-5 notif-2 notif-1]}}}] + (is (= {:db {:activity-center + {:filter {:type types/contact-request :status :all} + :notifications [new-notif-3 new-notif-2 notif-1]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/notifications-reconcile + cofx + [new-notif-2 ; will be updated + new-notif-3 ; will be inserted + new-notif-4 ; will be ignored because it's not a contact request + (assoc notif-5 :deleted true) ; will be removed + ]))))) (testing "Contact request tab + Unread filter" - (h/run-test-sync - (setup) - (let [notif-1 {:id "0x1" :read false :type types/contact-request} - notif-2 {:id "0x2" :read false :type types/contact-request} - new-notif-2 (assoc notif-2 :read true) - new-notif-3 {:id "0x3" :read false :type types/contact-request} - new-notif-4 {:id "0x4" :read true :type types/contact-request} - new-notif-5 {:id "0x5" :read true :type types/system} - notif-6 {:id "0x6" :read false :type types/contact-request}] - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:type types/contact-request :status :unread} - :notifications [notif-6 notif-2 notif-1]}]) - - (rf/dispatch - [:activity-center.notifications/reconcile - [new-notif-2 ; will be removed because it's read - new-notif-3 ; will be inserted - new-notif-4 ; will be ignored because it's read - new-notif-5 ; will be ignored because it's not a contact request - (assoc notif-6 :deleted true) ; will be removed - ]]) - - (is (= [new-notif-3 notif-1] - (get-in (h/db) [:activity-center :notifications])))))) + (let [notif-1 {:id "0x1" :read false :type types/contact-request} + notif-2 {:id "0x2" :read false :type types/contact-request} + new-notif-2 (assoc notif-2 :read true) + new-notif-3 {:id "0x3" :read false :type types/contact-request} + new-notif-4 {:id "0x4" :read true :type types/contact-request} + new-notif-5 {:id "0x5" :read true :type types/system} + notif-6 {:id "0x6" :read false :type types/contact-request} + cofx {:db {:activity-center + {:filter {:type types/contact-request + :status :unread} + :notifications [notif-6 notif-2 notif-1]}}}] + (is (= {:db {:activity-center + {:filter {:type types/contact-request + :status :unread} + :notifications [new-notif-3 notif-1]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/notifications-reconcile + cofx + [new-notif-2 ; will be removed because it's read + new-notif-3 ; will be inserted + new-notif-4 ; will be ignored because it's read + new-notif-5 ; will be ignored because it's not a contact request + (assoc notif-6 :deleted true) ; will be removed + ]))))) ;; Sorting by timestamp and ID is compatible with what the backend does when ;; returning paginated results. (testing "sorts notifications by timestamp and id in descending order" - (h/run-test-sync - (setup) - (let [notif-1 {:id "0x1" :timestamp 1} - notif-2 {:id "0x2" :timestamp 1} - notif-3 {:id "0x3" :timestamp 50} - notif-4 {:id "0x4" :timestamp 100} - notif-5 {:id "0x5" :timestamp 100} - new-notif-1 (assoc notif-1 :last-message {}) - new-notif-4 (assoc notif-4 :last-message {})] - (rf/dispatch [:test/assoc-in [:activity-center :notifications] - [notif-1 notif-3 notif-4 notif-2 notif-5]]) - - (rf/dispatch [:activity-center.notifications/reconcile [new-notif-1 new-notif-4]]) - - (is (= [notif-5 new-notif-4 notif-3 notif-2 new-notif-1] - (get-in (h/db) [:activity-center :notifications]))))))) + (let [notif-1 {:id "0x1" :timestamp 1} + notif-2 {:id "0x2" :timestamp 1} + notif-3 {:id "0x3" :timestamp 50} + notif-4 {:id "0x4" :timestamp 100} + notif-5 {:id "0x5" :timestamp 100} + new-notif-1 (assoc notif-1 :last-message {}) + new-notif-4 (assoc notif-4 :last-message {}) + cofx {:db {:activity-center + {:notifications [notif-1 notif-3 notif-4 notif-2 + notif-5]}}}] + (is (= {:db {:activity-center + {:notifications [notif-5 + new-notif-4 + notif-3 + notif-2 + new-notif-1]}} + :dispatch-n [[:activity-center.notifications/fetch-unread-count] + [:activity-center.notifications/fetch-pending-contact-requests]]} + (events/notifications-reconcile cofx [new-notif-1 new-notif-4])))))) (deftest remove-pending-contact-request-test (testing "removes notification from all related filter types and status" - (h/run-test-sync - (setup) - (let [author "0x99" - notif-1 {:id "0x1" :read true :type types/contact-request} - notif-2 {:id "0x2" :read false :type types/contact-request :author author} - notif-3 {:id "0x3" :read false :type types/private-group-chat :author author}] - (rf/dispatch [:test/assoc-in [:activity-center :notifications] - [notif-3 ; will be ignored because it's not a contact request - notif-2 ; will be removed - notif-1 ; will be ignored because it's not from the same author - ]]) - - (rf/dispatch [:activity-center/remove-pending-contact-request author]) - - (is (= [notif-3 notif-1] - (get-in (h/db) [:activity-center :notifications]))))))) + (let [author "0x99" + notif-1 {:id "0x1" :read true :type types/contact-request} + notif-2 {:id "0x2" :read false :type types/contact-request :author author} + notif-3 {:id "0x3" :read false :type types/private-group-chat :author author} + cofx {:db {:activity-center + {:notifications + [notif-3 ; will be ignored because it's not a contact + ; request + notif-2 ; will be removed + notif-1 ; will be ignored because it's not from the + ; same author + ]}}}] + (is (= {:db {:activity-center {:notifications [notif-3 notif-1]}}} + (events/notifications-remove-pending-contact-request cofx author)))))) ;;;; Notifications fetching and pagination -(deftest notifications-fetch-test +(deftest notifications-fetch-first-page-test (testing "fetches first page" - (h/run-test-sync - (setup) - (let [spy-queue (atom [])] - (h/stub-fx-with-callbacks - :json-rpc/call - :on-success - (constantly {:cursor "10" - :notifications [{:id "0x1" - :type types/one-to-one-chat - :read false - :chatId "0x9"}]})) - (h/spy-fx spy-queue :json-rpc/call) - - (rf/dispatch [:activity-center.notifications/fetch-first-page - {:filter-type types/one-to-one-chat}]) - - (is (= :unread (get-in (h/db) [:activity-center :filter :status]))) - (is (= "" (get-in @spy-queue [0 :args 0 :params 0 :cursor])) - "Should be called with empty cursor when fetching first page") - (is (= "10" (get-in (h/db) [:activity-center :cursor]))) - (is (= [{:chat-id "0x9" - :chat-name nil - :chat-type types/one-to-one-chat - :group-chat false - :id "0x1" - :public? false - :last-message nil - :message nil - :read false - :reply-message nil - :type types/one-to-one-chat}] - (get-in (h/db) [:activity-center :notifications])))))) + (let [cofx {:db {}}] + (is (= {:db {:activity-center {:filter {:type types/one-to-one-chat + :status :unread} + :loading? true}} + :json-rpc/call [{:method "wakuext_activityCenterNotifications" + :params [{:cursor "" + :limit (:notifications-per-page events/defaults) + :activityTypes [types/one-to-one-chat] + :readType events/read-type-unread}] + :on-success [:activity-center.notifications/fetch-success true] + :on-error [:activity-center.notifications/fetch-error + types/one-to-one-chat :unread]}]} + (events/notifications-fetch-first-page cofx {:filter-type types/one-to-one-chat})))))) +(deftest notifications-fetch-next-next-page-test (testing "does not fetch next page when pagination cursor reached the end" - (h/run-test-sync - (setup) - (let [spy-queue (atom [])] - (h/spy-fx spy-queue :json-rpc/call) + (is (nil? (events/notifications-fetch-next-page + {:db {:activity-center {:cursor events/start-or-end-cursor}}})))) - (rf/dispatch [:test/assoc-in [:activity-center :cursor] ""]) - (rf/dispatch [:activity-center.notifications/fetch-next-page]) - (is (= [] @spy-queue))))) + (testing "fetches the next page" + (let [f-args (atom []) + cursor "abc" + cofx {:db {:activity-center {:cursor cursor + :filter {:type types/one-to-one-chat + :status :unread}}}}] + (with-redefs [events/notifications-fetch + (fn [& args] + (reset! f-args args) + :result)] + (is (= :result (events/notifications-fetch-next-page cofx))) + (is (= [cofx + {:cursor cursor + :filter-type types/one-to-one-chat + :filter-status :unread + :reset-data? false}] + @f-args)))))) - (testing "fetches next page when pagination cursor is not empty" - (h/run-test-sync - (setup) - (let [spy-queue (atom [])] - (h/stub-fx-with-callbacks - :json-rpc/call - :on-success - (constantly {:cursor "" - :notifications [{:id "0x1" - :type types/mention - :read false - :chatId "0x9"}]})) - (h/spy-fx spy-queue :json-rpc/call) - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:status :unread :type types/mention} - :cursor "10"}]) - - (rf/dispatch [:activity-center.notifications/fetch-next-page]) - - (is (= "10" (get-in @spy-queue [0 :args 0 :params 0 :cursor])) - "Should be called with current cursor") - (is (= "" (get-in (h/db) [:activity-center :cursor]))) - (is (= [{:chat-id "0x9" - :chat-name nil - :chat-type 3 - :id "0x1" - :last-message nil - :message nil - :read false - :reply-message nil - :type types/mention}] - (get-in (h/db) [:activity-center :notifications])))))) - - (testing "resets loading state after error" - (h/run-test-sync - (setup) - (let [spy-queue (atom [])] - (h/stub-fx-with-callbacks :json-rpc/call :on-error (constantly :fake-error)) - (h/spy-event-fx spy-queue :activity-center.notifications/fetch-error) - (h/spy-fx spy-queue :json-rpc/call) - (rf/dispatch [:test/assoc-in [:activity-center] - {:filter {:status :unread :type types/one-to-one-chat} - :cursor ""}]) - - (rf/dispatch [:activity-center.notifications/fetch-first-page]) - - (is (nil? (get-in (h/db) [:activity-center :loading?]))) - (is (= [:activity-center.notifications/fetch-error - types/one-to-one-chat - :unread - :fake-error] - (:args (last @spy-queue)))))))) +(deftest notifications-fetch-error-test + (testing "resets loading state" + (let [cofx {:db {:activity-center + {:loading? true + :filter {:status :unread + :type types/one-to-one-chat} + :cursor ""}}}] + (is (= {:db {:activity-center {:filter {:status :unread + :type types/one-to-one-chat} + :cursor ""}}} + (events/notifications-fetch-error cofx :dummy-error))))))