diff --git a/src/quo2/components/tabs/tabs.cljs b/src/quo2/components/tabs/tabs.cljs index e0c63c3c4a..a987e2bf36 100644 --- a/src/quo2/components/tabs/tabs.cljs +++ b/src/quo2/components/tabs/tabs.cljs @@ -4,6 +4,7 @@ [quo2.components.tabs.tab :as tab] [reagent.core :as reagent] [status-im.ui.components.react :as react] + [status-im.utils.core :as utils] [status-im.utils.number :as number-utils])) (def default-tab-size 32) @@ -82,8 +83,8 @@ (let [maybe-mask-wrapper (if fade-end? [react/masked-view {:mask-element (reagent/as-element - [react/linear-gradient {:colors ["black" "transparent"] - :locations [(@fading :fade-end-percentage) 1] + [react/linear-gradient {:colors [:black :transparent] + :locations [(get @fading :fade-end-percentage) 1] :start {:x 0 :y 0} :end {:x 1 :y 0} :pointer-events :none @@ -99,7 +100,9 @@ :on-change :scroll-on-press? :size) - {:ref (partial reset! flat-list-ref) + (when scroll-on-press? + {:initial-scroll-index (utils/first-index #(= @active-tab-id (:id %)) data)}) + {:ref #(reset! flat-list-ref %) :extra-data (str @active-tab-id) :horizontal true :scroll-event-throttle scroll-event-throttle @@ -116,7 +119,7 @@ :layout-width layout-width :max-fade-percentage fade-end-percentage})] ;; Avoid unnecessary re-rendering. - (when (not= new-percentage (@fading :fade-end-percentage)) + (when (not= new-percentage (get @fading :fade-end-percentage)) (swap! fading assoc :fade-end-percentage new-percentage)))) (when on-scroll (on-scroll e))) diff --git a/src/status_im/activity_center/core.cljs b/src/status_im/activity_center/core.cljs index 2dad121816..49060ee928 100644 --- a/src/status_im/activity_center/core.cljs +++ b/src/status_im/activity_center/core.cljs @@ -1,5 +1,6 @@ (ns status-im.activity-center.core (:require [re-frame.core :as re-frame] + [status-im.constants :as constants] [status-im.data-store.activities :as data-store.activities] [status-im.ethereum.json-rpc :as json-rpc] [status-im.utils.fx :as fx] @@ -8,90 +9,117 @@ ;;;; Notification reconciliation (defn- update-notifications - [old new] - (let [ids-to-be-removed (->> new - (filter #(or (:dismissed %) (:accepted %))) - (map :id)) - grouped-new (apply dissoc (group-by :id new) ids-to-be-removed) - grouped-old (apply dissoc (group-by :id old) ids-to-be-removed)] - (->> (merge grouped-old grouped-new) - vals - (map first)))) + "Insert `new-notifications` in `db-notifications`. + + Although correct, this is a naive implementation for reconciling notifications + because for every notification in `new-notifications`, linear scans will be + performed to remove it and sorting will be performed for every new insertion. + If the number of existing notifications cached in the app db becomes + ~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)))))) + db-notifications + new-notifications)) (fx/defn notifications-reconcile {:events [:activity-center.notifications/reconcile]} [{:keys [db]} new-notifications] - (let [{read-new true - unread-new false} (group-by :read new-notifications) - read-old (get-in db [:activity-center :notifications-read :data]) - unread-old (get-in db [:activity-center :notifications-unread :data])] - {:db (-> db - (assoc-in [:activity-center :notifications-read :data] - (update-notifications read-old read-new)) - (assoc-in [:activity-center :notifications-unread :data] - (update-notifications unread-old unread-new)))})) + (when (seq new-notifications) + {:db (update-in db [:activity-center :notifications] + update-notifications new-notifications)})) ;;;; Notifications fetching and pagination -(def notifications-per-page - 20) +(def defaults + {:filter-status :unread + :filter-type constants/activity-center-notification-type-no-type + :notifications-per-page 10}) (def start-or-end-cursor "") -(defn notifications-group->rpc-method - [notifications-group] - (if (= notifications-group :notifications-read) +(defn- valid-cursor? + [cursor] + (and (some? cursor) + (not= cursor start-or-end-cursor))) + +(defn- filter-status->rpc-method + [filter-status] + (if (= filter-status :read) "wakuext_readActivityCenterNotifications" "wakuext_unreadActivityCenterNotifications")) -(defn notifications-read-status->group - [status-filter] - (if (= status-filter :read) - :notifications-read - :notifications-unread)) - (fx/defn notifications-fetch - [{:keys [db]} cursor notifications-group] - (when-not (get-in db [:activity-center notifications-group :loading?]) - {:db (assoc-in db [:activity-center notifications-group :loading?] true) - ::json-rpc/call [{:method (notifications-group->rpc-method notifications-group) - :params [cursor notifications-per-page] - :on-success #(re-frame/dispatch [:activity-center.notifications/fetch-success notifications-group %]) - :on-error #(re-frame/dispatch [:activity-center.notifications/fetch-error notifications-group %])}]})) + [{:keys [db]} {:keys [cursor filter-type filter-status reset-data?]}] + (when-not (get-in db [:activity-center :notifications filter-type filter-status :loading?]) + {: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 %])}]})) (fx/defn notifications-fetch-first-page {:events [:activity-center.notifications/fetch-first-page]} - [{:keys [db] :as cofx} {:keys [status-filter] :or {status-filter :unread}}] - (let [notifications-group (notifications-read-status->group status-filter)] + [{:keys [db] :as cofx} {:keys [filter-type filter-status]}] + (let [filter-type (or filter-type + (get-in db [:activity-center :filter :type] + (defaults :filter-type))) + filter-status (or filter-status + (get-in db [:activity-center :filter :status] + (defaults :filter-status)))] (fx/merge cofx {:db (-> db - (assoc-in [:activity-center :current-status-filter] status-filter) - (update-in [:activity-center notifications-group] dissoc :loading?) - (update-in [:activity-center notifications-group] dissoc :data))} - (notifications-fetch start-or-end-cursor notifications-group)))) + (assoc-in [:activity-center :filter :type] filter-type) + (assoc-in [:activity-center :filter :status] filter-status))} + (notifications-fetch {:cursor start-or-end-cursor + :filter-type filter-type + :filter-status filter-status + :reset-data? true})))) (fx/defn notifications-fetch-next-page {:events [:activity-center.notifications/fetch-next-page]} [{:keys [db] :as cofx}] - (let [status-filter (get-in db [:activity-center :current-status-filter]) - notifications-group (notifications-read-status->group status-filter) - {:keys [cursor]} (get-in db [:activity-center notifications-group])] - (when-not (= cursor start-or-end-cursor) - (notifications-fetch cofx cursor notifications-group)))) + (let [{:keys [type status]} (get-in db [:activity-center :filter]) + {:keys [cursor]} (get-in db [:activity-center :notifications type status])] + (when (valid-cursor? cursor) + (notifications-fetch cofx {:cursor cursor + :filter-type type + :filter-status status + :reset-data? false})))) (fx/defn notifications-fetch-success {:events [:activity-center.notifications/fetch-success]} - [{:keys [db]} notifications-group {:keys [cursor notifications]}] - {:db (-> db - (update-in [:activity-center notifications-group] dissoc :loading?) - (assoc-in [:activity-center notifications-group :cursor] cursor) - (update-in [:activity-center notifications-group :data] - concat - (map data-store.activities/<-rpc notifications)))}) + [{:keys [db]} + filter-type + filter-status + reset-data? + {:keys [cursor notifications]}] + (let [processed (map data-store.activities/<-rpc notifications)] + {:db (-> db + (assoc-in [:activity-center :notifications filter-type filter-status :cursor] cursor) + (update-in [:activity-center :notifications filter-type filter-status] dissoc :loading?) + (update-in [:activity-center :notifications filter-type filter-status :data] + (if reset-data? + (constantly processed) + #(concat %1 processed))))})) (fx/defn notifications-fetch-error {:events [:activity-center.notifications/fetch-error]} - [{:keys [db]} notifications-group error] + [{:keys [db]} filter-type filter-status error] (log/warn "Failed to load Activity Center notifications" error) - {:db (update-in db [:activity-center notifications-group] dissoc :loading?)}) + {:db (update-in db [:activity-center :notifications filter-type filter-status] dissoc :loading?)}) diff --git a/src/status_im/activity_center/core_test.cljs b/src/status_im/activity_center/core_test.cljs index 8822666c19..911cc3b5ae 100644 --- a/src/status_im/activity_center/core_test.cljs +++ b/src/status_im/activity_center/core_test.cljs @@ -2,99 +2,285 @@ (: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.ethereum.json-rpc :as json-rpc] - [status-im.test-helpers :as h] - status-im.events)) + status-im.events + [status-im.test-helpers :as h])) (defn setup [] (h/register-helper-events) (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." + [grouped-notifications {:keys [type status]}] + (update-in grouped-notifications + [type status :data] + (fn [old _] + (map #(dissoc % :color) old)) + nil)) + (deftest notifications-reconcile-test (testing "does nothing when there are no new notifications" (rf-test/run-test-sync (setup) - (let [read [{:id "0x1" :read true}] - unread [{:id "0x4" :read false}]] - (rf/dispatch [:test/assoc-in [:activity-center :notifications-read :data] read]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :data] unread]) + (let [notifications {c/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}]} + :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 + {:unread {:cursor "" + :data [{:id "0x4" + :read false + :type c/activity-center-notification-type-private-group-chat}]}}}] + (rf/dispatch [:test/assoc-in [:activity-center :notifications] notifications]) (rf/dispatch [:activity-center.notifications/reconcile nil]) - (is (= read (get-in (h/db) [:activity-center :notifications-read :data]))) - (is (= unread (get-in (h/db) [:activity-center :notifications-unread :data])))))) + (is (= notifications (get-in (h/db) [:activity-center :notifications])))))) (testing "removes dismissed or accepted notifications" (rf-test/run-test-sync (setup) - (rf/dispatch [:test/assoc-in [:activity-center :notifications-read :data] - [{:id "0x1" :read true} - {:id "0x2" :read true} - {:id "0x3" :read true}]]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :data] - [{:id "0x4" :read false} - {:id "0x5" :read false} - {:id "0x6" :read false}]]) + (rf/dispatch [:test/assoc-in [:activity-center :notifications] + {c/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}]} + :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}]}}}]) (rf/dispatch [:activity-center.notifications/reconcile - [{:id "0x1" :read true :dismissed true} - {:id "0x3" :read true :accepted true} - {:id "0x4" :read false :dismissed true} - {:id "0x5" :read false :accepted true}]]) + [{:id "0x1" + :read true + :type c/activity-center-notification-type-one-to-one-chat + :dismissed true} + {:id "0x3" + :read false + :type c/activity-center-notification-type-one-to-one-chat + :accepted true} + {:id "0x4" + :read false + :type c/activity-center-notification-type-private-group-chat + :dismissed true} + {:id "0x5" + :read false + :type c/activity-center-notification-type-private-group-chat + :accepted true}]]) - (is (= [{:id "0x2" :read true}] - (get-in (h/db) [:activity-center :notifications-read :data]))) - (is (= [{:id "0x6" :read false}] - (get-in (h/db) [:activity-center :notifications-unread :data]))))) + (is (= {c/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}]} + :unread {:cursor "" + :data []}} + c/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}]}}} + (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-read :data] - [{:id "0x1" :read true} - {:id "0x2" :read true}]]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :data] - [{:id "0x3" :read false} - {:id "0x4" :read false}]]) + (rf/dispatch [:test/assoc-in [:activity-center :notifications] + {c/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 + {:unread {:cursor "" + :data [{:id "0x4" + :read false + :type c/activity-center-notification-type-private-group-chat} + {:id "0x6" + :read false + :type c/activity-center-notification-type-private-group-chat}]}}}]) (rf/dispatch [:activity-center.notifications/reconcile - [{:id "0x1" :read true :name "ABC"} - {:id "0x3" :read false :name "XYZ"}]]) + [{:id "0x1" + :read true + :type c/activity-center-notification-type-one-to-one-chat + :last-message {}} + {:id "0x4" + :read false + :type c/activity-center-notification-type-private-group-chat + :author "0xabc"} + {:id "0x6" + :read false + :type c/activity-center-notification-type-private-group-chat}]]) - (is (= [{:id "0x1" :read true :name "ABC"} - {:id "0x2" :read true}] - (get-in (h/db) [:activity-center :notifications-read :data]))) - (is (= [{:id "0x3" :read false :name "XYZ"} - {:id "0x4" :read false}] - (get-in (h/db) [:activity-center :notifications-unread :data])))))) + (is (= {c/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 + :last-message {}}]} + :unread {:data []}} + c/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} + {:id "0x4" + :read false + :type c/activity-center-notification-type-private-group-chat + :author "0xabc"}]}}} + (get-in (h/db) [:activity-center :notifications]))))) + + (testing "reconciles notifications that switched their read/unread status" + (rf-test/run-test-sync + (setup) + (rf/dispatch [:test/assoc-in [:activity-center :notifications] + {c/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}]}}}]) + + (rf/dispatch [:activity-center.notifications/reconcile + [{:id "0x1" + :read false + :type c/activity-center-notification-type-one-to-one-chat}]]) + + (is (= {c/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}]}}} + (get-in (h/db) [:activity-center :notifications]))))) + + ;; 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" + (rf-test/run-test-sync + (setup) + (rf/dispatch [:test/assoc-in [:activity-center :notifications] + {c/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}]} + :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}]}}}]) + + (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 {}}]]) + + (is (= {c/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 + :timestamp 1} + {:id "0x1" + :read true + :type c/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 + :timestamp 100} + {:id "0x4" + :read false + :type c/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 + :timestamp 50}]}}} + (get-in (h/db) [:activity-center :notifications])))))) (deftest notifications-fetch-test (testing "fetches first page" (rf-test/run-test-sync (setup) (let [spy-queue (atom [])] - (h/stub-fx-with-callbacks ::json-rpc/call - :on-success (constantly {:cursor "10" - :notifications [{:chatId "0x1"}]})) + (h/stub-fx-with-callbacks + ::json-rpc/call + :on-success (constantly {:cursor "10" + :notifications [{:id "0x1" + :type c/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]) + (rf/dispatch [:activity-center.notifications/fetch-first-page + {:filter-type c/activity-center-notification-type-one-to-one-chat}]) - (is (= :unread (get-in (h/db) [:activity-center :current-status-filter]))) - (is (nil? (get-in (h/db) [:activity-center :notifications-unread :loading?]))) - (is (= "10" (get-in (h/db) [:activity-center :notifications-unread :cursor]))) - (is (= "" (get-in @spy-queue [0 :args 0 :params 0]))) - (is (= [{:chat-id "0x1"}] - (->> (get-in (h/db) [:activity-center :notifications-unread :data]) - (map #(select-keys % [:chat-id])))))))) + (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 + {:unread {:cursor "10" + :data [{:chat-id "0x9" + :chat-name nil + :chat-type c/activity-center-notification-type-one-to-one-chat + :group-chat false + :id "0x1" + :public? false + :last-message nil + :message nil + :read false + :reply-message nil + :type c/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})))))) (testing "does not fetch next page when pagination cursor reached the end" (rf-test/run-test-sync (setup) (let [spy-queue (atom [])] (h/spy-fx spy-queue ::json-rpc/call) - (rf/dispatch [:test/assoc-in [:activity-center :current-status-filter] :unread]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :cursor] ""]) + (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] + ""]) + + (rf/dispatch [:activity-center.notifications/fetch-next-page]) + + (is (= [] @spy-queue))))) + + ;; The cursor can be nil sometimes because the reconciliation doesn't care + ;; about updating the cursor value, but we have to make sure the next page is + ;; only fetched if the current cursor is valid. + (testing "does not fetch next page when cursor is nil" + (rf-test/run-test-sync + (setup) + (let [spy-queue (atom [])] + (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-one-to-one-chat]) + (rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :unread :cursor] + nil]) (rf/dispatch [:activity-center.notifications/fetch-next-page]) @@ -104,30 +290,54 @@ (rf-test/run-test-sync (setup) (let [spy-queue (atom [])] - (h/stub-fx-with-callbacks ::json-rpc/call - :on-success (constantly {:cursor "" - :notifications [{:chatId "0x1"}]})) + (h/stub-fx-with-callbacks + ::json-rpc/call + :on-success (constantly {:cursor "" + :notifications [{:id "0x1" + :type c/activity-center-notification-type-mention + :read false + :chatId "0x9"}]})) (h/spy-fx spy-queue ::json-rpc/call) - (rf/dispatch [:test/assoc-in [:activity-center :current-status-filter] :unread]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :cursor] "10"]) + (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] + "10"]) (rf/dispatch [:activity-center.notifications/fetch-next-page]) (is (= "wakuext_unreadActivityCenterNotifications" (get-in @spy-queue [0 :args 0 :method]))) - (is (= "10" (get-in @spy-queue [0 :args 0 :params 0]))) - (is (= "" (get-in (h/db) [:activity-center :notifications-unread :cursor]))) - (is (= [{:chat-id "0x1"}] - (->> (get-in (h/db) [:activity-center :notifications-unread :data]) - (map #(select-keys % [:chat-id])))))))) + (is (= "10" (get-in @spy-queue [0 :args 0 :params 0])) + "Should be called with current cursor") + (is (= {c/activity-center-notification-type-mention + {:unread {:cursor "" + :data [{:chat-id "0x9" + :chat-name nil + :chat-type 3 + :id "0x1" + :last-message nil + :message nil + :read false + :reply-message nil + :type c/activity-center-notification-type-mention}]}}} + (remove-color-key (get-in (h/db) [:activity-center :notifications]) + {:status :unread + :type c/activity-center-notification-type-mention})))))) (testing "does not fetch next page while it is still loading" (rf-test/run-test-sync (setup) (let [spy-queue (atom [])] (h/spy-fx spy-queue ::json-rpc/call) - (rf/dispatch [:test/assoc-in [:activity-center :current-status-filter] :read]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications-read :cursor] "10"]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications-read :loading?] true]) + (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] + "10"]) + (rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :read :loading?] + true]) (rf/dispatch [:activity-center.notifications/fetch-next-page]) @@ -140,13 +350,18 @@ (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 :current-status-filter] :unread]) - (rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :cursor] ""]) + (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] + ""]) (rf/dispatch [:activity-center.notifications/fetch-first-page]) - (is (nil? (get-in (h/db) [:activity-center :notifications-unread :loading?]))) + (is (nil? (get-in (h/db) [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :unread :loading?]))) (is (= [:activity-center.notifications/fetch-error - :notifications-unread + c/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 fcb7e9e68e..4b44a89f04 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -176,6 +176,7 @@ (def ^:const docs-link "https://status.im/docs/") (def ^:const principles-link "https://our.status.im/our-principles/") +(def ^:const activity-center-notification-type-no-type 0) (def ^:const activity-center-notification-type-one-to-one-chat 1) (def ^:const activity-center-notification-type-private-group-chat 2) (def ^:const activity-center-notification-type-mention 3) @@ -183,6 +184,13 @@ (def ^:const activity-center-notification-type-contact-request 5) (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) + (def ^:const visibility-status-unknown 0) (def ^:const visibility-status-automatic 1) (def ^:const visibility-status-dnd 2) diff --git a/src/status_im/subs/activity_center.cljs b/src/status_im/subs/activity_center.cljs index 5b7e3a5559..2ee31776f1 100644 --- a/src/status_im/subs/activity_center.cljs +++ b/src/status_im/subs/activity_center.cljs @@ -6,35 +6,36 @@ [clojure.string :as string])) (re-frame/reg-sub - :activity-center/notifications-read - (fn [db] - (get-in db [:activity-center :notifications-read :data]))) + :activity-center/notifications + :<- [:activity-center] + (fn [activity-center] + (:notifications activity-center))) (re-frame/reg-sub - :activity-center/notifications-unread - (fn [db] - (get-in db [:activity-center :notifications-unread :data]))) + :activity-center/filter-status + :<- [:activity-center] + (fn [activity-center] + (get-in activity-center [:filter :status]))) (re-frame/reg-sub - :activity-center/current-status-filter - (fn [db] - (get-in db [:activity-center :current-status-filter]))) + :activity-center/filter-type + :<- [:activity-center] + (fn [activity-center] + (get-in activity-center [:filter :type] constants/activity-center-notification-type-no-type))) (re-frame/reg-sub - :activity-center/status-filter-unread-enabled? - :<- [:activity-center/current-status-filter] - (fn [current-status-filter] - (= :unread current-status-filter))) + :activity-center/filtered-notifications + :<- [:activity-center/filter-type] + :<- [:activity-center/filter-status] + :<- [:activity-center/notifications] + (fn [[filter-type filter-status notifications]] + (get-in notifications [filter-type filter-status :data]))) (re-frame/reg-sub - :activity-center/notifications-per-read-status - :<- [:activity-center/notifications-read] - :<- [:activity-center/notifications-unread] - :<- [:activity-center/status-filter-unread-enabled?] - (fn [[notifications-read notifications-unread unread-filter-enabled?]] - (if unread-filter-enabled? - notifications-unread - notifications-read))) + :activity-center/filter-status-unread-enabled? + :<- [:activity-center/filter-status] + (fn [filter-status] + (= :unread filter-status))) (defn- group-notifications-by-date [notifications] diff --git a/src/status_im/subs/root.cljs b/src/status_im/subs/root.cljs index ff4c70edd2..f8cb68add7 100644 --- a/src/status_im/subs/root.cljs +++ b/src/status_im/subs/root.cljs @@ -228,6 +228,7 @@ (reg-root-key-sub :activity.center/notifications :activity.center/notifications) (reg-root-key-sub :activity.center/notifications-count :activity.center/notifications-count) +(reg-root-key-sub :activity-center :activity-center) (reg-root-key-sub :bug-report/description-error :bug-report/description-error) (reg-root-key-sub :bug-report/details :bug-report/details) diff --git a/src/status_im/ui/screens/activity_center/views.cljs b/src/status_im/ui/screens/activity_center/views.cljs index cf9d9657ca..d9253c5946 100644 --- a/src/status_im/ui/screens/activity_center/views.cljs +++ b/src/status_im/ui/screens/activity_center/views.cljs @@ -1,5 +1,6 @@ (ns status-im.ui.screens.activity-center.views (:require [quo.components.animated.pressable :as animation] + [quo.design-system.colors :as quo.colors] [quo.react-native :as rn] [quo2.components.buttons.button :as button] [quo2.components.markdown.text :as text] @@ -14,9 +15,6 @@ [status-im.utils.datetime :as datetime] [status-im.utils.handlers :refer [evt]])) -(defonce selected-activity-type - (reagent/atom :activity-type/all)) - (defn activity-title [{:keys [type]}] (case type @@ -32,8 +30,8 @@ [{:keys [type]}] (case type constants/activity-center-notification-type-contact-request - :add-user - :placeholder)) + :main-icons2/add-user + :main-icons2/placeholder)) (defn activity-context [{:keys [message last-message type]}] @@ -81,7 +79,7 @@ nil)) (defn activity-pressable - [notification & children] + [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 @@ -89,14 +87,13 @@ ;; `: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 #(>evt [:contact.ui/send-message-pressed {:public-key (:author notification)}])} - children] - [:<> children])) + activity] + activity)) (defn render-notification [notification index] - [rn/view {:flex 1 - :flex-direction :column - :margin-top (if (= 0 index) 0 4)} + [rn/view {:margin-top (if (= 0 index) 0 4) + :padding-horizontal 20} [activity-pressable notification [activity-logs/activity-log (merge {:context (activity-context notification) @@ -108,67 +105,108 @@ :unread? (not (:read notification))} (activity-buttons notification))]]]) -(defn notifications-list +(defn filter-selector-read-toggle [] - (let [notifications (evt [:activity-center.notifications/fetch-next-page]) - :render-fn render-notification}])) - -(defn filter-selector-read [] - (let [unread-filter-enabled? (evt [:activity-center.notifications/fetch-first-page {:status-filter :read}]) - (>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}]))} - :unread])) + :on-press #(>evt [:activity-center.notifications/fetch-first-page + {:filter-status (if unread-filter-enabled? + :read + :unread)}])} + :main-icons2/unread])) -(defn activity-center [] +;; TODO(2022-10-07): The empty state is still under design analysis, so we +;; shouldn't even care about translations at this point. A placeholder box is +;; used instead of an image. +(defn empty-tab + [] + [rn/view {:style {:align-items :center + :flex 1 + :justify-content :center + :padding-vertical 12}} + [rn/view {:style {:background-color colors/neutral-80 + :height 120 + :margin-bottom 20 + :width 120}}] + [text/text {:size :paragraph-1 + :style {:padding-bottom 2} + :weight :semi-bold} + "No notifications"] + [text/text {:size :paragraph-2} + "Your notifications will be here"]]) + +(defn tabs + [] + (let [filter-type (evt [:activity-center.notifications/fetch-first-page {:filter-type %}]) + :default-active filter-type + :data [{:id constants/activity-center-notification-type-no-type + :label (i18n/label :t/all)} + {:id constants/activity-center-notification-type-admin + :label (i18n/label :t/admin)} + {:id constants/activity-center-notification-type-mention + :label (i18n/label :t/mentions)} + {:id constants/activity-center-notification-type-reply + :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 + :label (i18n/label :t/identity-verification)} + {:id constants/activity-center-notification-type-tx + :label (i18n/label :t/transactions)} + {:id constants/activity-center-notification-type-membership + :label (i18n/label :t/membership)} + {:id constants/activity-center-notification-type-system + :label (i18n/label :t/system)}]}])) + +(defn header + [] + (let [screen-padding 20] + ;; TODO: Remove temporary (and old) background color when the screen and + ;; header are properly blurred. + [rn/view {:background-color (:ui-background @quo.colors/theme)} + [button/button {:icon true + :type :grey + :size 32 + :style {:margin-vertical 12 + :margin-left screen-padding} + :on-press #(>evt [:navigate-back])} + :main-icons2/close] + [text/text {:size :heading-1 + :weight :semi-bold + :style {:padding-horizontal screen-padding + :padding-vertical 12}} + (i18n/label :t/notifications)] + [rn/view {:flex-direction :row + :padding-vertical 12} + [rn/view {:flex 1 + :align-self :stretch} + [tabs]] + [rn/view {:flex-grow 0 + :margin-left 16 + :padding-right screen-padding} + [filter-selector-read-toggle]]]])) + +(defn activity-center + [] (reagent/create-class - {:component-did-mount #(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}]) + {:component-did-mount #(>evt [:activity-center.notifications/fetch-first-page]) :reagent-render (fn [] - (let [screen-padding 20] - [:<> - [button/button {:icon true - :type :grey - :size 32 - :style {:margin-vertical 12 - :margin-left screen-padding} - :on-press #(>evt [:navigate-back])} - :close] - [text/text {:size :heading-1 - :weight :semi-bold - :style {:padding-horizontal screen-padding - :padding-vertical 12}} - (i18n/label :t/notifications)] - [rn/view {:flex-direction :row - :padding-vertical 12} - [rn/view {:flex 1 - :align-self :stretch} - [tabs/scrollable-tabs {:size 32 - :style {:padding-left screen-padding} - :fade-end-percentage 0.79 - :scroll-on-press? true - :fade-end? true - :on-change (partial reset! selected-activity-type) - :default-active :activity-type/all - :data [{:id :activity-type/all :label (i18n/label :t/all)} - {:id :activity-type/admin :label (i18n/label :t/admin)} - {:id :activity-type/mention :label (i18n/label :t/mentions)} - {:id :activity-type/reply :label (i18n/label :t/replies)} - {:id :activity-type/contact-request :label (i18n/label :t/contact-requests)} - {:id :activity-type/identity-verification :label (i18n/label :t/identity-verification)} - {:id :activity-type/transaction :label (i18n/label :t/transactions)} - {:id :activity-type/membership :label (i18n/label :t/membership)} - {:id :activity-type/system :label (i18n/label :t/system)}]}]] - [rn/view {:flex-grow 0 - :margin-left 16 - :padding-right screen-padding} - [filter-selector-read]]] - [rn/view {:padding-horizontal screen-padding} - [notifications-list]]]))})) + (let [notifications (evt [:activity-center.notifications/fetch-next-page]) + :render-fn render-notification + :sticky-header-indices [0]}]))}))