diff --git a/src/status_im/activity_center/core.cljs b/src/status_im/activity_center/core.cljs index 49060ee928..30105a5d9c 100644 --- a/src/status_im/activity_center/core.cljs +++ b/src/status_im/activity_center/core.cljs @@ -1,6 +1,6 @@ (ns status-im.activity-center.core - (:require [re-frame.core :as re-frame] - [status-im.constants :as constants] + (:require [re-frame.core :as rf] + [status-im.constants :as const] [status-im.data-store.activities :as data-store.activities] [status-im.ethereum.json-rpc :as json-rpc] [status-im.utils.fx :as fx] @@ -18,21 +18,23 @@ ~excessively~ big, this implementation will probably need to be revisited." [db-notifications new-notifications] (reduce (fn [acc {:keys [id type read] :as notification}] - (let [filter-status (if read :read :unread)] - (cond-> (-> acc - (update-in [type :read :data] - (fn [data] - (remove #(= id (:id %)) data))) - (update-in [type :unread :data] - (fn [data] - (remove #(= id (:id %)) data)))) - (not (or (:dismissed notification) (:accepted notification))) - (update-in [type filter-status :data] - (fn [data] - (->> notification - (conj data) - (sort-by (juxt :timestamp :id)) - reverse)))))) + (let [filter-status (if read :read :unread) + remove-notification (fn [data] + (remove #(= id (:id %)) data)) + insert-and-sort (fn [data] + (->> notification + (conj data) + (sort-by (juxt :timestamp :id)) + reverse))] + (as-> acc $ + (update-in $ [type :read :data] remove-notification) + (update-in $ [type :unread :data] remove-notification) + (update-in $ [const/activity-center-notification-type-no-type :read :data] remove-notification) + (update-in $ [const/activity-center-notification-type-no-type :unread :data] remove-notification) + (if (or (:dismissed notification) (:accepted notification)) + $ + (-> $ (update-in [type filter-status :data] insert-and-sort) + (update-in [const/activity-center-notification-type-no-type filter-status :data] insert-and-sort)))))) db-notifications new-notifications)) @@ -43,11 +45,37 @@ {:db (update-in db [:activity-center :notifications] update-notifications new-notifications)})) +;;;; Contact verification + +(fx/defn contact-verification-decline + {:events [:activity-center.contact-verification/decline]} + [_ contact-verification-id] + {::json-rpc/call [{:method "wakuext_declineContactVerificationRequest" + :params [contact-verification-id] + :on-success #(rf/dispatch [:activity-center.contact-verification/decline-success %]) + :on-error #(rf/dispatch [:activity-center.contact-verification/decline-error contact-verification-id %])}]}) + +(fx/defn contact-verification-decline-success + {:events [:activity-center.contact-verification/decline-success]} + [cofx response] + (->> response + :activityCenterNotifications + (map data-store.activities/<-rpc) + (notifications-reconcile cofx))) + +(fx/defn contact-verification-decline-error + {:events [:activity-center.contact-verification/decline-error]} + [_ contact-verification-id error] + (log/warn "Failed to decline contact verification" + {:contact-verification-id contact-verification-id + :error error}) + nil) + ;;;; Notifications fetching and pagination (def defaults {:filter-status :unread - :filter-type constants/activity-center-notification-type-no-type + :filter-type const/activity-center-notification-type-no-type :notifications-per-page 10}) (def start-or-end-cursor @@ -70,8 +98,8 @@ {:db (assoc-in db [:activity-center :notifications filter-type filter-status :loading?] true) ::json-rpc/call [{:method (filter-status->rpc-method filter-status) :params [cursor (defaults :notifications-per-page) filter-type] - :on-success #(re-frame/dispatch [:activity-center.notifications/fetch-success filter-type filter-status reset-data? %]) - :on-error #(re-frame/dispatch [:activity-center.notifications/fetch-error filter-type filter-status %])}]})) + :on-success #(rf/dispatch [:activity-center.notifications/fetch-success filter-type filter-status reset-data? %]) + :on-error #(rf/dispatch [:activity-center.notifications/fetch-error filter-type filter-status %])}]})) (fx/defn notifications-fetch-first-page {:events [:activity-center.notifications/fetch-first-page]} diff --git a/src/status_im/activity_center/core_test.cljs b/src/status_im/activity_center/core_test.cljs index 911cc3b5ae..8e01560b60 100644 --- a/src/status_im/activity_center/core_test.cljs +++ b/src/status_im/activity_center/core_test.cljs @@ -2,7 +2,7 @@ (:require [cljs.test :refer [deftest is testing]] [day8.re-frame.test :as rf-test] [re-frame.core :as rf] - [status-im.constants :as c] + [status-im.constants :as const] [status-im.ethereum.json-rpc :as json-rpc] status-im.events [status-im.test-helpers :as h])) @@ -12,8 +12,8 @@ (rf/dispatch [:init/app-started])) (defn remove-color-key - "Remove `:color` key from notifications because they have random values that we - can't assert against." + "Remove `:color` key from notifications because they have random values that are + inconvenient to assert against." [grouped-notifications {:keys [type status]}] (update-in grouped-notifications [type status :data] @@ -21,27 +21,117 @@ (map #(dissoc % :color) old)) nil)) +;;;; Contact verification + +(deftest contact-verification-decline-test + (testing "successfully declines and reconciles returned notification" + (rf-test/run-test-sync + (setup) + (let [spy-queue (atom []) + contact-verification-id 24 + expected-notification {:accepted false + :author "0x04d03f" + :chat-id "0x04d03f" + :contact-verification-status 3 + :dismissed false + :id 24 + :last-message nil + :message {:command-parameters nil + :content {:chat-id nil + :ens-name nil + :image nil + :line-count nil + :links nil + :parsed-text nil + :response-to nil + :rtl? nil + :sticker nil + :text nil} + :outgoing false + :outgoing-status nil + :quoted-message nil} + :name "0x04d03f" + :read true + :reply-message nil + :timestamp 1666647286000 + :type const/activity-center-notification-type-contact-verification}] + (h/stub-fx-with-callbacks + ::json-rpc/call + :on-success (constantly {:activityCenterNotifications + [{:accepted false + :author "0x04d03f" + :chatId "0x04d03f" + :contactVerificationStatus 3 + :dismissed false + :id contact-verification-id + :message {} + :name "0x04d03f" + :read true + :timestamp 1666647286000 + :type const/activity-center-notification-type-contact-verification}]})) + + (h/spy-fx spy-queue ::json-rpc/call) + + (rf/dispatch [:activity-center.contact-verification/decline contact-verification-id]) + + (is (= {:method "wakuext_declineContactVerificationRequest" + :params [contact-verification-id]} + (-> @spy-queue + (get-in [0 :args 0]) + (select-keys [:method :params])))) + + (is (= {const/activity-center-notification-type-no-type + {:read {:data [expected-notification]} + :unread {:data []}} + const/activity-center-notification-type-contact-verification + {:read {:data [expected-notification]} + :unread {:data []}}} + (-> (h/db) + (get-in [:activity-center :notifications]) + (remove-color-key {:type const/activity-center-notification-type-no-type + :status :read}) + (remove-color-key {:type const/activity-center-notification-type-contact-verification + :status :read}))))))) + + (testing "logs failure" + (rf-test/run-test-sync + (setup) + (let [contact-verification-id 666] + (h/using-log-test-appender + (fn [logs] + (h/stub-fx-with-callbacks ::json-rpc/call :on-error (constantly :fake-error)) + + (rf/dispatch [:activity-center.contact-verification/decline contact-verification-id]) + + (is (= {:args ["Failed to decline contact verification" + {:contact-verification-id contact-verification-id + :error :fake-error}] + :level :warn} + (last @logs))))))))) + +;;;; Notification reconciliation + (deftest notifications-reconcile-test (testing "does nothing when there are no new notifications" (rf-test/run-test-sync (setup) - (let [notifications {c/activity-center-notification-type-one-to-one-chat + (let [notifications {const/activity-center-notification-type-one-to-one-chat {:read {:cursor "" :data [{:id "0x1" :read true - :type c/activity-center-notification-type-one-to-one-chat} + :type const/activity-center-notification-type-one-to-one-chat} {:id "0x2" :read true - :type c/activity-center-notification-type-one-to-one-chat}]} + :type const/activity-center-notification-type-one-to-one-chat}]} :unread {:cursor "" :data [{:id "0x3" :read false - :type c/activity-center-notification-type-one-to-one-chat}]}} - c/activity-center-notification-type-private-group-chat + :type const/activity-center-notification-type-one-to-one-chat}]}} + const/activity-center-notification-type-private-group-chat {:unread {:cursor "" :data [{:id "0x4" :read false - :type c/activity-center-notification-type-private-group-chat}]}}}] + :type const/activity-center-notification-type-private-group-chat}]}}}] (rf/dispatch [:test/assoc-in [:activity-center :notifications] notifications]) (rf/dispatch [:activity-center.notifications/reconcile nil]) @@ -52,96 +142,127 @@ (rf-test/run-test-sync (setup) (rf/dispatch [:test/assoc-in [:activity-center :notifications] - {c/activity-center-notification-type-one-to-one-chat + {const/activity-center-notification-type-one-to-one-chat {:read {:cursor "" - :data [{:id "0x1" :read true :type c/activity-center-notification-type-one-to-one-chat} - {:id "0x2" :read true :type c/activity-center-notification-type-one-to-one-chat}]} + :data [{:id "0x1" :read true :type const/activity-center-notification-type-one-to-one-chat} + {:id "0x2" :read true :type const/activity-center-notification-type-one-to-one-chat}]} :unread {:cursor "" - :data [{:id "0x3" :read false :type c/activity-center-notification-type-one-to-one-chat}]}} - 2 {:unread {:cursor "" - :data [{:id "0x4" :read false :type 2} - {:id "0x6" :read false :type 2}]}}}]) + :data [{:id "0x3" :read false :type const/activity-center-notification-type-one-to-one-chat}]}} + const/activity-center-notification-type-private-group-chat + {:unread {:cursor "" + :data [{:id "0x4" :read false :type const/activity-center-notification-type-private-group-chat} + {:id "0x6" :read false :type const/activity-center-notification-type-private-group-chat}]}}}]) (rf/dispatch [:activity-center.notifications/reconcile [{:id "0x1" :read true - :type c/activity-center-notification-type-one-to-one-chat + :type const/activity-center-notification-type-one-to-one-chat :dismissed true} {:id "0x3" :read false - :type c/activity-center-notification-type-one-to-one-chat + :type const/activity-center-notification-type-one-to-one-chat :accepted true} {:id "0x4" :read false - :type c/activity-center-notification-type-private-group-chat + :type const/activity-center-notification-type-private-group-chat :dismissed true} {:id "0x5" :read false - :type c/activity-center-notification-type-private-group-chat + :type const/activity-center-notification-type-private-group-chat :accepted true}]]) - (is (= {c/activity-center-notification-type-one-to-one-chat + (is (= {const/activity-center-notification-type-no-type + {:read {:data []} + :unread {:data []}} + + const/activity-center-notification-type-one-to-one-chat {:read {:cursor "" :data [{:id "0x2" :read true - :type c/activity-center-notification-type-one-to-one-chat}]} + :type const/activity-center-notification-type-one-to-one-chat}]} :unread {:cursor "" :data []}} - c/activity-center-notification-type-private-group-chat + const/activity-center-notification-type-private-group-chat {:read {:data []} :unread {:cursor "" :data [{:id "0x6" :read false - :type c/activity-center-notification-type-private-group-chat}]}}} + :type const/activity-center-notification-type-private-group-chat}]}}} (get-in (h/db) [:activity-center :notifications]))))) (testing "replaces old notifications with newly arrived ones" (rf-test/run-test-sync (setup) (rf/dispatch [:test/assoc-in [:activity-center :notifications] - {c/activity-center-notification-type-one-to-one-chat + {const/activity-center-notification-type-no-type + {:read {:cursor "" + :data [{:id "0x1" + :read true + :type const/activity-center-notification-type-one-to-one-chat}]} + :unread {:cursor "" + :data [{:id "0x4" + :read false + :type const/activity-center-notification-type-private-group-chat} + {:id "0x6" + :read false + :type const/activity-center-notification-type-private-group-chat}]}} + const/activity-center-notification-type-one-to-one-chat {:read {:cursor "" :data [{:id "0x1" :read true - :type c/activity-center-notification-type-one-to-one-chat}]}} - c/activity-center-notification-type-private-group-chat + :type const/activity-center-notification-type-one-to-one-chat}]}} + const/activity-center-notification-type-private-group-chat {:unread {:cursor "" :data [{:id "0x4" :read false - :type c/activity-center-notification-type-private-group-chat} + :type const/activity-center-notification-type-private-group-chat} {:id "0x6" :read false - :type c/activity-center-notification-type-private-group-chat}]}}}]) + :type const/activity-center-notification-type-private-group-chat}]}}}]) (rf/dispatch [:activity-center.notifications/reconcile [{:id "0x1" :read true - :type c/activity-center-notification-type-one-to-one-chat + :type const/activity-center-notification-type-one-to-one-chat :last-message {}} {:id "0x4" :read false - :type c/activity-center-notification-type-private-group-chat + :type const/activity-center-notification-type-private-group-chat :author "0xabc"} {:id "0x6" :read false - :type c/activity-center-notification-type-private-group-chat}]]) + :type const/activity-center-notification-type-private-group-chat}]]) - (is (= {c/activity-center-notification-type-one-to-one-chat + (is (= {const/activity-center-notification-type-no-type {:read {:cursor "" :data [{:id "0x1" :read true - :type c/activity-center-notification-type-one-to-one-chat + :type const/activity-center-notification-type-one-to-one-chat + :last-message {}}]} + :unread {:cursor "" + :data [{:id "0x6" + :read false + :type const/activity-center-notification-type-private-group-chat} + {:id "0x4" + :read false + :type const/activity-center-notification-type-private-group-chat + :author "0xabc"}]}} + const/activity-center-notification-type-one-to-one-chat + {:read {:cursor "" + :data [{:id "0x1" + :read true + :type const/activity-center-notification-type-one-to-one-chat :last-message {}}]} :unread {:data []}} - c/activity-center-notification-type-private-group-chat + const/activity-center-notification-type-private-group-chat {:read {:data []} :unread {:cursor "" :data [{:id "0x6" :read false - :type c/activity-center-notification-type-private-group-chat} + :type const/activity-center-notification-type-private-group-chat} {:id "0x4" :read false - :type c/activity-center-notification-type-private-group-chat + :type const/activity-center-notification-type-private-group-chat :author "0xabc"}]}}} (get-in (h/db) [:activity-center :notifications]))))) @@ -149,23 +270,29 @@ (rf-test/run-test-sync (setup) (rf/dispatch [:test/assoc-in [:activity-center :notifications] - {c/activity-center-notification-type-one-to-one-chat + {const/activity-center-notification-type-one-to-one-chat {:read {:cursor "" :data [{:id "0x1" :read true - :type c/activity-center-notification-type-one-to-one-chat}]}}}]) + :type const/activity-center-notification-type-one-to-one-chat}]}}}]) (rf/dispatch [:activity-center.notifications/reconcile [{:id "0x1" :read false - :type c/activity-center-notification-type-one-to-one-chat}]]) + :type const/activity-center-notification-type-one-to-one-chat}]]) - (is (= {c/activity-center-notification-type-one-to-one-chat + (is (= {const/activity-center-notification-type-no-type + {:read {:data []} + :unread {:data [{:id "0x1" + :read false + :type const/activity-center-notification-type-one-to-one-chat}]}} + + const/activity-center-notification-type-one-to-one-chat {:read {:cursor "" :data []} :unread {:data [{:id "0x1" :read false - :type c/activity-center-notification-type-one-to-one-chat}]}}} + :type const/activity-center-notification-type-one-to-one-chat}]}}} (get-in (h/db) [:activity-center :notifications]))))) ;; Sorting by timestamp and ID is compatible with what the backend does when @@ -174,46 +301,59 @@ (rf-test/run-test-sync (setup) (rf/dispatch [:test/assoc-in [:activity-center :notifications] - {c/activity-center-notification-type-one-to-one-chat + {const/activity-center-notification-type-one-to-one-chat {:read {:cursor "" - :data [{:id "0x1" :read true :type c/activity-center-notification-type-one-to-one-chat :timestamp 1} - {:id "0x2" :read true :type c/activity-center-notification-type-one-to-one-chat :timestamp 1}]} + :data [{:id "0x1" :read true :type const/activity-center-notification-type-one-to-one-chat :timestamp 1} + {:id "0x2" :read true :type const/activity-center-notification-type-one-to-one-chat :timestamp 1}]} :unread {:cursor "" - :data [{:id "0x3" :read false :type c/activity-center-notification-type-one-to-one-chat :timestamp 50} - {:id "0x4" :read false :type c/activity-center-notification-type-one-to-one-chat :timestamp 100} - {:id "0x5" :read false :type c/activity-center-notification-type-one-to-one-chat :timestamp 100}]}}}]) + :data [{:id "0x3" :read false :type const/activity-center-notification-type-one-to-one-chat :timestamp 50} + {:id "0x4" :read false :type const/activity-center-notification-type-one-to-one-chat :timestamp 100} + {:id "0x5" :read false :type const/activity-center-notification-type-one-to-one-chat :timestamp 100}]}}}]) (rf/dispatch [:activity-center.notifications/reconcile - [{:id "0x1" :read true :type c/activity-center-notification-type-one-to-one-chat :timestamp 1 :last-message {}} - {:id "0x4" :read false :type c/activity-center-notification-type-one-to-one-chat :timestamp 100 :last-message {}}]]) + [{:id "0x1" :read true :type const/activity-center-notification-type-one-to-one-chat :timestamp 1 :last-message {}} + {:id "0x4" :read false :type const/activity-center-notification-type-one-to-one-chat :timestamp 100 :last-message {}}]]) - (is (= {c/activity-center-notification-type-one-to-one-chat + (is (= {const/activity-center-notification-type-no-type + {:read {:data [{:id "0x1" + :read true + :type const/activity-center-notification-type-one-to-one-chat + :timestamp 1 + :last-message {}}]} + :unread {:data [{:id "0x4" + :read false + :type const/activity-center-notification-type-one-to-one-chat + :timestamp 100 + :last-message {}}]}} + const/activity-center-notification-type-one-to-one-chat {:read {:cursor "" :data [{:id "0x2" :read true - :type c/activity-center-notification-type-one-to-one-chat + :type const/activity-center-notification-type-one-to-one-chat :timestamp 1} {:id "0x1" :read true - :type c/activity-center-notification-type-one-to-one-chat + :type const/activity-center-notification-type-one-to-one-chat :timestamp 1 :last-message {}}]} :unread {:cursor "" :data [{:id "0x5" :read false - :type c/activity-center-notification-type-one-to-one-chat + :type const/activity-center-notification-type-one-to-one-chat :timestamp 100} {:id "0x4" :read false - :type c/activity-center-notification-type-one-to-one-chat + :type const/activity-center-notification-type-one-to-one-chat :timestamp 100 :last-message {}} {:id "0x3" :read false - :type c/activity-center-notification-type-one-to-one-chat + :type const/activity-center-notification-type-one-to-one-chat :timestamp 50}]}}} (get-in (h/db) [:activity-center :notifications])))))) +;;;; Notifications fetching and pagination + (deftest notifications-fetch-test (testing "fetches first page" (rf-test/run-test-sync @@ -223,22 +363,22 @@ ::json-rpc/call :on-success (constantly {:cursor "10" :notifications [{:id "0x1" - :type c/activity-center-notification-type-one-to-one-chat + :type const/activity-center-notification-type-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 c/activity-center-notification-type-one-to-one-chat}]) + {:filter-type const/activity-center-notification-type-one-to-one-chat}]) (is (= :unread (get-in (h/db) [:activity-center :filter :status]))) (is (= "" (get-in @spy-queue [0 :args 0 :params 0])) "Should be called with empty cursor when fetching first page") - (is (= {c/activity-center-notification-type-one-to-one-chat + (is (= {const/activity-center-notification-type-one-to-one-chat {:unread {:cursor "10" :data [{:chat-id "0x9" :chat-name nil - :chat-type c/activity-center-notification-type-one-to-one-chat + :chat-type const/activity-center-notification-type-one-to-one-chat :group-chat false :id "0x1" :public? false @@ -246,10 +386,10 @@ :message nil :read false :reply-message nil - :type c/activity-center-notification-type-one-to-one-chat}]}}} + :type const/activity-center-notification-type-one-to-one-chat}]}}} (remove-color-key (get-in (h/db) [:activity-center :notifications]) {:status :unread - :type c/activity-center-notification-type-one-to-one-chat})))))) + :type const/activity-center-notification-type-one-to-one-chat})))))) (testing "does not fetch next page when pagination cursor reached the end" (rf-test/run-test-sync @@ -259,8 +399,8 @@ (rf/dispatch [:test/assoc-in [:activity-center :filter :status] :unread]) (rf/dispatch [:test/assoc-in [:activity-center :filter :type] - c/activity-center-notification-type-one-to-one-chat]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :unread :cursor] + const/activity-center-notification-type-one-to-one-chat]) + (rf/dispatch [:test/assoc-in [:activity-center :notifications const/activity-center-notification-type-one-to-one-chat :unread :cursor] ""]) (rf/dispatch [:activity-center.notifications/fetch-next-page]) @@ -278,8 +418,8 @@ (rf/dispatch [:test/assoc-in [:activity-center :filter :status] :unread]) (rf/dispatch [:test/assoc-in [:activity-center :filter :type] - c/activity-center-notification-type-one-to-one-chat]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :unread :cursor] + const/activity-center-notification-type-one-to-one-chat]) + (rf/dispatch [:test/assoc-in [:activity-center :notifications const/activity-center-notification-type-one-to-one-chat :unread :cursor] nil]) (rf/dispatch [:activity-center.notifications/fetch-next-page]) @@ -294,15 +434,15 @@ ::json-rpc/call :on-success (constantly {:cursor "" :notifications [{:id "0x1" - :type c/activity-center-notification-type-mention + :type const/activity-center-notification-type-mention :read false :chatId "0x9"}]})) (h/spy-fx spy-queue ::json-rpc/call) (rf/dispatch [:test/assoc-in [:activity-center :filter :status] :unread]) (rf/dispatch [:test/assoc-in [:activity-center :filter :type] - c/activity-center-notification-type-mention]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-mention :unread :cursor] + const/activity-center-notification-type-mention]) + (rf/dispatch [:test/assoc-in [:activity-center :notifications const/activity-center-notification-type-mention :unread :cursor] "10"]) (rf/dispatch [:activity-center.notifications/fetch-next-page]) @@ -310,7 +450,7 @@ (is (= "wakuext_unreadActivityCenterNotifications" (get-in @spy-queue [0 :args 0 :method]))) (is (= "10" (get-in @spy-queue [0 :args 0 :params 0])) "Should be called with current cursor") - (is (= {c/activity-center-notification-type-mention + (is (= {const/activity-center-notification-type-mention {:unread {:cursor "" :data [{:chat-id "0x9" :chat-name nil @@ -320,10 +460,10 @@ :message nil :read false :reply-message nil - :type c/activity-center-notification-type-mention}]}}} + :type const/activity-center-notification-type-mention}]}}} (remove-color-key (get-in (h/db) [:activity-center :notifications]) {:status :unread - :type c/activity-center-notification-type-mention})))))) + :type const/activity-center-notification-type-mention})))))) (testing "does not fetch next page while it is still loading" (rf-test/run-test-sync @@ -333,10 +473,10 @@ (rf/dispatch [:test/assoc-in [:activity-center :filter :status] :read]) (rf/dispatch [:test/assoc-in [:activity-center :filter :type] - c/activity-center-notification-type-one-to-one-chat]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :read :cursor] + const/activity-center-notification-type-one-to-one-chat]) + (rf/dispatch [:test/assoc-in [:activity-center :notifications const/activity-center-notification-type-one-to-one-chat :read :cursor] "10"]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :read :loading?] + (rf/dispatch [:test/assoc-in [:activity-center :notifications const/activity-center-notification-type-one-to-one-chat :read :loading?] true]) (rf/dispatch [:activity-center.notifications/fetch-next-page]) @@ -353,15 +493,15 @@ (rf/dispatch [:test/assoc-in [:activity-center :filter :status] :unread]) (rf/dispatch [:test/assoc-in [:activity-center :filter :type] - c/activity-center-notification-type-one-to-one-chat]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :unread :cursor] + const/activity-center-notification-type-one-to-one-chat]) + (rf/dispatch [:test/assoc-in [:activity-center :notifications const/activity-center-notification-type-one-to-one-chat :unread :cursor] ""]) (rf/dispatch [:activity-center.notifications/fetch-first-page]) - (is (nil? (get-in (h/db) [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :unread :loading?]))) + (is (nil? (get-in (h/db) [:activity-center :notifications const/activity-center-notification-type-one-to-one-chat :unread :loading?]))) (is (= [:activity-center.notifications/fetch-error - c/activity-center-notification-type-one-to-one-chat + const/activity-center-notification-type-one-to-one-chat :unread :fake-error] (:args (last @spy-queue)))))))) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index ae63163afe..a5bd57fa94 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -22,6 +22,13 @@ (def ^:const contact-request-state-received 3) (def ^:const contact-request-state-dismissed 4) +(def ^:const contact-verification-state-unknown 0) +(def ^:const contact-verification-state-pending 1) +(def ^:const contact-verification-state-accepted 2) +(def ^:const contact-verification-state-declined 3) +(def ^:const contact-verification-state-cancelled 4) +(def ^:const contact-verification-state-trusted 5) + (def ^:const emoji-reaction-love 1) (def ^:const emoji-reaction-thumbs-up 2) (def ^:const emoji-reaction-thumbs-down 3) @@ -183,11 +190,15 @@ (def ^:const activity-center-notification-type-mention 3) (def ^:const activity-center-notification-type-reply 4) (def ^:const activity-center-notification-type-contact-request 5) +(def ^:const activity-center-notification-type-contact-verification 6) + +;; TODO: Remove this constant once the old Notification Center code is removed. +;; Its value clashes with the new constant `activity-center-notification-type-contact-verification` +;; used in status-go. (def ^:const activity-center-notification-type-contact-request-retracted 6) ;; TODO: Replace with correct enum values once status-go implements them. (def ^:const activity-center-notification-type-admin 66610) -(def ^:const activity-center-notification-type-identity-verification 66611) (def ^:const activity-center-notification-type-tx 66612) (def ^:const activity-center-notification-type-membership 66613) (def ^:const activity-center-notification-type-system 66614) diff --git a/src/status_im/data_store/activities.cljs b/src/status_im/data_store/activities.cljs index 4a183c1b3e..2e26ebe283 100644 --- a/src/status_im/data_store/activities.cljs +++ b/src/status_im/data_store/activities.cljs @@ -1,44 +1,44 @@ (ns status-im.data-store.activities - (:require [status-im.data-store.messages :as messages] - [status-im.constants :as constants] + (:require [clojure.set :as set] [quo.design-system.colors :as colors] - clojure.set)) + [status-im.constants :as constants] + [status-im.data-store.messages :as messages])) -(defn rpc->type [{:keys [type name] :as chat}] - (cond - (= constants/activity-center-notification-type-reply type) +(defn- rpc->type [{:keys [type name] :as chat}] + (case type + constants/activity-center-notification-type-reply (assoc chat :chat-name name :chat-type constants/private-group-chat-type) - (= constants/activity-center-notification-type-mention type) + constants/activity-center-notification-type-mention (assoc chat :chat-type constants/private-group-chat-type :chat-name name) - (= constants/activity-center-notification-type-private-group-chat type) + constants/activity-center-notification-type-private-group-chat (assoc chat :chat-type constants/private-group-chat-type :chat-name name :public? false :group-chat true) - (= constants/activity-center-notification-type-one-to-one-chat type) + constants/activity-center-notification-type-one-to-one-chat (assoc chat :chat-type constants/one-to-one-chat-type :chat-name name :public? false :group-chat false) - :else chat)) (defn <-rpc [item] (-> item rpc->type - (clojure.set/rename-keys {:lastMessage :last-message - :replyMessage :reply-message - :chatId :chat-id}) + (set/rename-keys {:lastMessage :last-message + :replyMessage :reply-message + :chatId :chat-id + :contactVerificationStatus :contact-verification-status}) (assoc :color (rand-nth colors/chat-colors)) (update :last-message #(when % (messages/<-rpc %))) (update :message #(when % (messages/<-rpc %))) diff --git a/src/status_im/data_store/activities_test.cljs b/src/status_im/data_store/activities_test.cljs new file mode 100644 index 0000000000..b79ec109dd --- /dev/null +++ b/src/status_im/data_store/activities_test.cljs @@ -0,0 +1,101 @@ +(ns status-im.data-store.activities-test + (:require [cljs.test :refer [deftest is testing]] + [quo.design-system.colors :as colors] + [status-im.constants :as constants] + [status-im.data-store.activities :as store])) + +(def chat-id + "0x04c66155") + +(def chat-name + "0x04c661") + +(def raw-notification + {:chatId chat-id + :contactVerificationStatus constants/contact-verification-state-pending + :lastMessage {} + :name chat-name + :replyMessage {}}) + +(deftest <-rpc-test + (testing "assocs random chat color" + (is (contains? (set colors/chat-colors) (:color (store/<-rpc raw-notification))))) + + (testing "renames keys" + (is (= {:name chat-name + :chat-id chat-id + :contact-verification-status constants/contact-verification-state-pending} + (-> raw-notification + store/<-rpc + (dissoc :color :last-message :message :reply-message))))) + + (testing "transforms messages from RPC response" + (is (= {:last-message {:quoted-message nil + :outgoing-status nil + :command-parameters nil + :content {:sticker nil + :rtl? nil + :ens-name nil + :parsed-text nil + :response-to nil + :chat-id nil + :image nil + :line-count nil + :links nil + :text nil} + :outgoing false} + :message nil + :reply-message {:quoted-message nil + :outgoing-status nil + :command-parameters nil + :content {:sticker nil + :rtl? nil + :ens-name nil + :parsed-text nil + :response-to nil + :chat-id nil + :image nil + :line-count nil + :links nil + :text nil} + :outgoing false}} + (-> raw-notification + store/<-rpc + (select-keys [:last-message :message :reply-message]))))) + + (testing "augments notification based on its type" + (is (= {:chat-name chat-name + :chat-type constants/private-group-chat-type + :name chat-name} + (-> raw-notification + (assoc :type constants/activity-center-notification-type-reply) + store/<-rpc + (select-keys [:name :chat-type :chat-name :public? :group-chat])))) + + (is (= {:chat-name chat-name + :chat-type constants/private-group-chat-type + :name chat-name} + (-> raw-notification + (assoc :type constants/activity-center-notification-type-mention) + store/<-rpc + (select-keys [:name :chat-type :chat-name :public? :group-chat])))) + + (is (= {:chat-name chat-name + :chat-type constants/private-group-chat-type + :group-chat true + :name chat-name + :public? false} + (-> raw-notification + (assoc :type constants/activity-center-notification-type-private-group-chat) + store/<-rpc + (select-keys [:name :chat-type :chat-name :public? :group-chat])))) + + (is (= {:chat-name chat-name + :chat-type constants/one-to-one-chat-type + :group-chat false + :name chat-name + :public? false} + (-> raw-notification + (assoc :type constants/activity-center-notification-type-one-to-one-chat) + store/<-rpc + (select-keys [:name :chat-type :chat-name :public? :group-chat])))))) diff --git a/src/status_im/data_store/messages.cljs b/src/status_im/data_store/messages.cljs index 065e868afd..f7f805c420 100644 --- a/src/status_im/data_store/messages.cljs +++ b/src/status_im/data_store/messages.cljs @@ -20,6 +20,7 @@ (clojure.set/rename-keys {:id :message-id :whisperTimestamp :whisper-timestamp :editedAt :edited-at + :contactVerificationState :contact-verification-state :contactRequestState :contact-request-state :commandParameters :command-parameters :gapParameters :gap-parameters diff --git a/src/status_im/data_store/messages_test.cljs b/src/status_im/data_store/messages_test.cljs index 854c0b1852..b5b42d0ca3 100644 --- a/src/status_im/data_store/messages_test.cljs +++ b/src/status_im/data_store/messages_test.cljs @@ -20,6 +20,8 @@ :response-to "a" :links nil} :whisper-timestamp 1 + :contact-verification-state 1 + :contact-request-state 2 :outgoing-status :sending :command-parameters nil :outgoing true @@ -35,6 +37,8 @@ :whisperTimestamp 1 :parsedText "parsed-text" :ensName "ens-name" + :contactVerificationState 1 + :contactRequestState 2 :localChatId chat-id :from from :text "hta" diff --git a/src/status_im/test_helpers.cljs b/src/status_im/test_helpers.cljs index 590cb63ebf..be62cbbb87 100644 --- a/src/status_im/test_helpers.cljs +++ b/src/status_im/test_helpers.cljs @@ -6,9 +6,10 @@ prefer to use it for more general purpose concepts, such as the re-frame event layer." (:require [re-frame.core :as rf] - [re-frame.registrar :as rf-registrar] + [re-frame.db :as rf-db] [re-frame.events :as rf-events] - [re-frame.db :as rf-db])) + [re-frame.registrar :as rf-registrar] + [taoensso.timbre :as log])) (defn db "A simple wrapper to get the latest value from the app db." @@ -85,3 +86,17 @@ (original-on-error (on-error fx-map)) (and original-on-success on-success) (original-on-success (on-success fx-map))))))) + +(defn using-log-test-appender + "Rebinds `taoensso.timbre/*config*` to use a custom test appender that persists + all `taoensso.timbre/log` call arguments. `f` is called with the atom + reference so that tests can de-reference it and verify log messages and their + respective levels." + [f] + (let [logs (atom [])] + (binding [log/*config* (assoc-in log/*config* + [:appenders :test] + {:enabled? true + :fn (fn [{:keys [vargs level]}] + (swap! logs conj {:args vargs :level level}))})] + (f logs)))) diff --git a/src/status_im/ui/screens/activity_center/views.cljs b/src/status_im/ui/screens/activity_center/views.cljs index a213e22d37..8ddeacb31a 100644 --- a/src/status_im/ui/screens/activity_center/views.cljs +++ b/src/status_im/ui/screens/activity_center/views.cljs @@ -15,98 +15,114 @@ [status-im.utils.handlers :refer [evt]] [quo.components.safe-area :as safe-area])) -(defn activity-title - [{:keys [type]}] - (case type - constants/activity-center-notification-type-contact-request - (i18n/label :t/contact-request) +;;;; Misc - constants/activity-center-notification-type-one-to-one-chat - "Dummy 1:1 chat title" +(defn sender-name + [contact] + (or (get-in contact [:names :nickname]) + (get-in contact [:names :three-words-name]))) - "Dummy default title")) +(defmulti notification-component :type) -(defn activity-icon - [{:keys [type]}] - (case type - constants/activity-center-notification-type-contact-request - :main-icons2/add-user - :main-icons2/placeholder)) +;;;; Contact request notifications -(defn activity-context - [{:keys [message last-message type]}] - (case type - constants/activity-center-notification-type-contact-request - (let [message (or message last-message) - contact (evt [:hide-popover]) + (>evt [:contact.ui/send-message-pressed {:public-key (:author notification)}]))}] + [:<>])] + (conj pressable + [activity-logs/activity-log + (merge {:title (i18n/label :t/contact-request) + :icon :main-icons2/add-user + :timestamp (datetime/timestamp->relative (:timestamp notification)) + :unread? (not (:read notification)) + :context [[context-tags/user-avatar-tag + {:color :purple + :override-theme :dark + :size :small + :style {:background-color colors/white-opa-10} + :text-style {:color colors/white}} + (sender-name contact) + (multiaccounts/displayed-photo contact)] + [rn/text {:style {:color colors/white}} + (i18n/label :t/contact-request-sent)]] + :message {:body (get-in message [:content :text])} + :status (case (:contact-request-state message) + constants/contact-request-message-state-accepted + {:type :positive :label (i18n/label :t/accepted)} + constants/contact-request-message-state-declined + {:type :negative :label (i18n/label :t/declined)} + nil)} + (case (:contact-request-state message) + constants/contact-request-state-mutual + {:button-1 {:label (i18n/label :t/decline) + :type :danger + :on-press #(>evt [:contact-requests.ui/decline-request id])} + :button-2 {:label (i18n/label :t/message-reply) + :type :success + :override-background-color colors/success-60 + :on-press #(>evt [:contact-requests.ui/accept-request id])}} + nil))]))) -(defn activity-message - [{:keys [message last-message]}] - {:body (get-in (or message last-message) [:content :text])}) +;;;; Contact verification notifications -(defn activity-status - [notification] - (case (get-in notification [:message :contact-request-state]) - constants/contact-request-message-state-accepted - {:type :positive :label (i18n/label :t/accepted)} - constants/contact-request-message-state-declined - {:type :negative :label (i18n/label :t/declined)} - nil)) +(defmethod notification-component constants/activity-center-notification-type-contact-verification + [{:keys [id contact-verification-status] :as notification}] + (let [message (or (:message notification) (:last-notification notification)) + contact (relative (:timestamp notification)) + :unread? (not (:read notification)) + :context [[context-tags/user-avatar-tag + {:color :purple + :override-theme :dark + :size :small + :style {:background-color colors/white-opa-10} + :text-style {:color colors/white}} + (sender-name contact) + (multiaccounts/displayed-photo contact)] + [rn/text {:style {:color colors/white}} + (str (i18n/label :t/identity-verification-request-sent) + ":")]] + :message (case contact-verification-status + (constants/contact-verification-state-pending + constants/contact-verification-state-declined) + {:body (get-in message [:content :text])} + nil) + :status (case contact-verification-status + constants/contact-verification-state-declined + {:type :negative :label (i18n/label :t/declined)} + nil)} + (case contact-verification-status + constants/contact-verification-state-pending + {:button-1 {:label (i18n/label :t/decline) + :type :danger + :on-press #(>evt [:activity-center.contact-verification/decline id])} + :button-2 {:label (i18n/label :t/accept) + :type :primary + ;; TODO: The acceptance flow will be implemented in follow-up PRs. + :on-press identity}} + nil))])) -(defn activity-buttons - [{:keys [id type]}] - (case type - constants/activity-center-notification-type-contact-request - {:button-1 {:label (i18n/label :t/decline) - :type :danger - :on-press #(>evt [:contact-requests.ui/decline-request id])} - :button-2 {:label (i18n/label :t/accept) - :type :success - :override-background-color colors/success-60 - :on-press #(>evt [:contact-requests.ui/accept-request id])}} - nil)) - -(defn activity-pressable - [notification activity] - (case (get-in notification [:message :contact-request-state]) - constants/contact-request-message-state-accepted - ;; NOTE [2022-09-21]: We need to dispatch to - ;; `:contact.ui/send-message-pressed` instead of - ;; `:chat.ui/navigate-to-chat`, otherwise the chat screen looks completely - ;; broken if it has never been opened before for the accepted contact. - [animation/pressable {:on-press (fn [] - (>evt [:hide-popover]) - (>evt [:contact.ui/send-message-pressed {:public-key (:author notification)}]))} - activity] - activity)) +;;;; Type-independent components (defn render-notification [notification index] [rn/view {:margin-top (if (= 0 index) 0 4) :padding-horizontal 20} - [activity-pressable notification - [activity-logs/activity-log - (merge {:context (activity-context notification) - :icon (activity-icon notification) - :message (activity-message notification) - :status (activity-status notification) - :timestamp (datetime/timestamp->relative (:timestamp notification)) - :title (activity-title notification) - :unread? (not (:read notification))} - (activity-buttons notification))]]]) + [notification-component notification]]) (defn filter-selector-read-toggle [] @@ -164,7 +180,7 @@ :label (i18n/label :t/replies)} {:id constants/activity-center-notification-type-contact-request :label (i18n/label :t/contact-requests)} - {:id constants/activity-center-notification-type-identity-verification + {:id constants/activity-center-notification-type-contact-verification :label (i18n/label :t/identity-verification)} {:id constants/activity-center-notification-type-tx :label (i18n/label :t/transactions)} diff --git a/translations/en.json b/translations/en.json index ec03ac80fa..7e26a20f90 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1819,6 +1819,8 @@ "admin": "Admin", "replies": "Replies", "identity-verification": "Identity verification", + "identity-verification-request": "Identity verification request", + "identity-verification-request-sent": "asked you", "membership": "Membership", "jump-to": "Jump to", "blank-messages-text": "Your messages will be here",