[#13968] Fetch and reconcile notifications by type and read status (#14125)

- Extract tabs component from the main screen component to reduce rendering cost when switching tabs.
- Display empty state component when there are no notifications.
- Fix screen flickering due to quickly flushing and filling the db state.
- Display top bar as a sticky header.
- Namespace icon keywords.
- Correctly sorts notifications during reconciliation.
- Remove warning about children without unique key.
This commit is contained in:
Icaro Motta 2022-10-10 16:33:42 -03:00 committed by GitHub
parent adfb0ddb5d
commit af5f979443
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 509 additions and 215 deletions

View File

@ -4,6 +4,7 @@
[quo2.components.tabs.tab :as tab] [quo2.components.tabs.tab :as tab]
[reagent.core :as reagent] [reagent.core :as reagent]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im.utils.core :as utils]
[status-im.utils.number :as number-utils])) [status-im.utils.number :as number-utils]))
(def default-tab-size 32) (def default-tab-size 32)
@ -82,8 +83,8 @@
(let [maybe-mask-wrapper (if fade-end? (let [maybe-mask-wrapper (if fade-end?
[react/masked-view [react/masked-view
{:mask-element (reagent/as-element {:mask-element (reagent/as-element
[react/linear-gradient {:colors ["black" "transparent"] [react/linear-gradient {:colors [:black :transparent]
:locations [(@fading :fade-end-percentage) 1] :locations [(get @fading :fade-end-percentage) 1]
:start {:x 0 :y 0} :start {:x 0 :y 0}
:end {:x 1 :y 0} :end {:x 1 :y 0}
:pointer-events :none :pointer-events :none
@ -99,7 +100,9 @@
:on-change :on-change
:scroll-on-press? :scroll-on-press?
:size) :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) :extra-data (str @active-tab-id)
:horizontal true :horizontal true
:scroll-event-throttle scroll-event-throttle :scroll-event-throttle scroll-event-throttle
@ -116,7 +119,7 @@
:layout-width layout-width :layout-width layout-width
:max-fade-percentage fade-end-percentage})] :max-fade-percentage fade-end-percentage})]
;; Avoid unnecessary re-rendering. ;; 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)))) (swap! fading assoc :fade-end-percentage new-percentage))))
(when on-scroll (when on-scroll
(on-scroll e))) (on-scroll e)))

View File

@ -1,5 +1,6 @@
(ns status-im.activity-center.core (ns status-im.activity-center.core
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[status-im.constants :as constants]
[status-im.data-store.activities :as data-store.activities] [status-im.data-store.activities :as data-store.activities]
[status-im.ethereum.json-rpc :as json-rpc] [status-im.ethereum.json-rpc :as json-rpc]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
@ -8,90 +9,117 @@
;;;; Notification reconciliation ;;;; Notification reconciliation
(defn- update-notifications (defn- update-notifications
[old new] "Insert `new-notifications` in `db-notifications`.
(let [ids-to-be-removed (->> new
(filter #(or (:dismissed %) (:accepted %))) Although correct, this is a naive implementation for reconciling notifications
(map :id)) because for every notification in `new-notifications`, linear scans will be
grouped-new (apply dissoc (group-by :id new) ids-to-be-removed) performed to remove it and sorting will be performed for every new insertion.
grouped-old (apply dissoc (group-by :id old) ids-to-be-removed)] If the number of existing notifications cached in the app db becomes
(->> (merge grouped-old grouped-new) ~excessively~ big, this implementation will probably need to be revisited."
vals [db-notifications new-notifications]
(map first)))) (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 (fx/defn notifications-reconcile
{:events [:activity-center.notifications/reconcile]} {:events [:activity-center.notifications/reconcile]}
[{:keys [db]} new-notifications] [{:keys [db]} new-notifications]
(let [{read-new true (when (seq new-notifications)
unread-new false} (group-by :read new-notifications) {:db (update-in db [:activity-center :notifications]
read-old (get-in db [:activity-center :notifications-read :data]) update-notifications new-notifications)}))
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)))}))
;;;; Notifications fetching and pagination ;;;; Notifications fetching and pagination
(def notifications-per-page (def defaults
20) {:filter-status :unread
:filter-type constants/activity-center-notification-type-no-type
:notifications-per-page 10})
(def start-or-end-cursor (def start-or-end-cursor
"") "")
(defn notifications-group->rpc-method (defn- valid-cursor?
[notifications-group] [cursor]
(if (= notifications-group :notifications-read) (and (some? cursor)
(not= cursor start-or-end-cursor)))
(defn- filter-status->rpc-method
[filter-status]
(if (= filter-status :read)
"wakuext_readActivityCenterNotifications" "wakuext_readActivityCenterNotifications"
"wakuext_unreadActivityCenterNotifications")) "wakuext_unreadActivityCenterNotifications"))
(defn notifications-read-status->group
[status-filter]
(if (= status-filter :read)
:notifications-read
:notifications-unread))
(fx/defn notifications-fetch (fx/defn notifications-fetch
[{:keys [db]} cursor notifications-group] [{:keys [db]} {:keys [cursor filter-type filter-status reset-data?]}]
(when-not (get-in db [:activity-center notifications-group :loading?]) (when-not (get-in db [:activity-center :notifications filter-type filter-status :loading?])
{:db (assoc-in db [:activity-center notifications-group :loading?] true) {:db (assoc-in db [:activity-center :notifications filter-type filter-status :loading?] true)
::json-rpc/call [{:method (notifications-group->rpc-method notifications-group) ::json-rpc/call [{:method (filter-status->rpc-method filter-status)
:params [cursor notifications-per-page] :params [cursor (defaults :notifications-per-page) filter-type]
:on-success #(re-frame/dispatch [:activity-center.notifications/fetch-success notifications-group %]) :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 notifications-group %])}]})) :on-error #(re-frame/dispatch [:activity-center.notifications/fetch-error filter-type filter-status %])}]}))
(fx/defn notifications-fetch-first-page (fx/defn notifications-fetch-first-page
{:events [:activity-center.notifications/fetch-first-page]} {:events [:activity-center.notifications/fetch-first-page]}
[{:keys [db] :as cofx} {:keys [status-filter] :or {status-filter :unread}}] [{:keys [db] :as cofx} {:keys [filter-type filter-status]}]
(let [notifications-group (notifications-read-status->group status-filter)] (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 (fx/merge cofx
{:db (-> db {:db (-> db
(assoc-in [:activity-center :current-status-filter] status-filter) (assoc-in [:activity-center :filter :type] filter-type)
(update-in [:activity-center notifications-group] dissoc :loading?) (assoc-in [:activity-center :filter :status] filter-status))}
(update-in [:activity-center notifications-group] dissoc :data))} (notifications-fetch {:cursor start-or-end-cursor
(notifications-fetch start-or-end-cursor notifications-group)))) :filter-type filter-type
:filter-status filter-status
:reset-data? true}))))
(fx/defn notifications-fetch-next-page (fx/defn notifications-fetch-next-page
{:events [:activity-center.notifications/fetch-next-page]} {:events [:activity-center.notifications/fetch-next-page]}
[{:keys [db] :as cofx}] [{:keys [db] :as cofx}]
(let [status-filter (get-in db [:activity-center :current-status-filter]) (let [{:keys [type status]} (get-in db [:activity-center :filter])
notifications-group (notifications-read-status->group status-filter) {:keys [cursor]} (get-in db [:activity-center :notifications type status])]
{:keys [cursor]} (get-in db [:activity-center notifications-group])] (when (valid-cursor? cursor)
(when-not (= cursor start-or-end-cursor) (notifications-fetch cofx {:cursor cursor
(notifications-fetch cofx cursor notifications-group)))) :filter-type type
:filter-status status
:reset-data? false}))))
(fx/defn notifications-fetch-success (fx/defn notifications-fetch-success
{:events [:activity-center.notifications/fetch-success]} {:events [:activity-center.notifications/fetch-success]}
[{:keys [db]} notifications-group {:keys [cursor notifications]}] [{:keys [db]}
{:db (-> db filter-type
(update-in [:activity-center notifications-group] dissoc :loading?) filter-status
(assoc-in [:activity-center notifications-group :cursor] cursor) reset-data?
(update-in [:activity-center notifications-group :data] {:keys [cursor notifications]}]
concat (let [processed (map data-store.activities/<-rpc notifications)]
(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 (fx/defn notifications-fetch-error
{:events [:activity-center.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) (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?)})

View File

@ -2,99 +2,285 @@
(:require [cljs.test :refer [deftest is testing]] (:require [cljs.test :refer [deftest is testing]]
[day8.re-frame.test :as rf-test] [day8.re-frame.test :as rf-test]
[re-frame.core :as rf] [re-frame.core :as rf]
[status-im.constants :as c]
[status-im.ethereum.json-rpc :as json-rpc] [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 [] (defn setup []
(h/register-helper-events) (h/register-helper-events)
(rf/dispatch [:init/app-started])) (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 (deftest notifications-reconcile-test
(testing "does nothing when there are no new notifications" (testing "does nothing when there are no new notifications"
(rf-test/run-test-sync (rf-test/run-test-sync
(setup) (setup)
(let [read [{:id "0x1" :read true}] (let [notifications {c/activity-center-notification-type-one-to-one-chat
unread [{:id "0x4" :read false}]] {:read {:cursor ""
(rf/dispatch [:test/assoc-in [:activity-center :notifications-read :data] read]) :data [{:id "0x1"
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :data] unread]) :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]) (rf/dispatch [:activity-center.notifications/reconcile nil])
(is (= read (get-in (h/db) [:activity-center :notifications-read :data]))) (is (= notifications (get-in (h/db) [:activity-center :notifications]))))))
(is (= unread (get-in (h/db) [:activity-center :notifications-unread :data]))))))
(testing "removes dismissed or accepted notifications" (testing "removes dismissed or accepted notifications"
(rf-test/run-test-sync (rf-test/run-test-sync
(setup) (setup)
(rf/dispatch [:test/assoc-in [:activity-center :notifications-read :data] (rf/dispatch [:test/assoc-in [:activity-center :notifications]
[{:id "0x1" :read true} {c/activity-center-notification-type-one-to-one-chat
{:id "0x2" :read true} {:read {:cursor ""
{:id "0x3" :read true}]]) :data [{:id "0x1" :read true :type c/activity-center-notification-type-one-to-one-chat}
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :data] {:id "0x2" :read true :type c/activity-center-notification-type-one-to-one-chat}]}
[{:id "0x4" :read false} :unread {:cursor ""
{:id "0x5" :read false} :data [{:id "0x3" :read false :type c/activity-center-notification-type-one-to-one-chat}]}}
{:id "0x6" :read false}]]) 2 {:unread {:cursor ""
:data [{:id "0x4" :read false :type 2}
{:id "0x6" :read false :type 2}]}}}])
(rf/dispatch [:activity-center.notifications/reconcile (rf/dispatch [:activity-center.notifications/reconcile
[{:id "0x1" :read true :dismissed true} [{:id "0x1"
{:id "0x3" :read true :accepted true} :read true
{:id "0x4" :read false :dismissed true} :type c/activity-center-notification-type-one-to-one-chat
{:id "0x5" :read false :accepted true}]]) :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}] (is (= {c/activity-center-notification-type-one-to-one-chat
(get-in (h/db) [:activity-center :notifications-read :data]))) {:read {:cursor ""
(is (= [{:id "0x6" :read false}] :data [{:id "0x2"
(get-in (h/db) [:activity-center :notifications-unread :data]))))) :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" (testing "replaces old notifications with newly arrived ones"
(rf-test/run-test-sync (rf-test/run-test-sync
(setup) (setup)
(rf/dispatch [:test/assoc-in [:activity-center :notifications-read :data] (rf/dispatch [:test/assoc-in [:activity-center :notifications]
[{:id "0x1" :read true} {c/activity-center-notification-type-one-to-one-chat
{:id "0x2" :read true}]]) {:read {:cursor ""
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :data] :data [{:id "0x1"
[{:id "0x3" :read false} :read true
{:id "0x4" :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}
{:id "0x6"
:read false
:type c/activity-center-notification-type-private-group-chat}]}}}])
(rf/dispatch [:activity-center.notifications/reconcile (rf/dispatch [:activity-center.notifications/reconcile
[{:id "0x1" :read true :name "ABC"} [{:id "0x1"
{:id "0x3" :read false :name "XYZ"}]]) :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"} (is (= {c/activity-center-notification-type-one-to-one-chat
{:id "0x2" :read true}] {:read {:cursor ""
(get-in (h/db) [:activity-center :notifications-read :data]))) :data [{:id "0x1"
(is (= [{:id "0x3" :read false :name "XYZ"} :read true
{:id "0x4" :read false}] :type c/activity-center-notification-type-one-to-one-chat
(get-in (h/db) [:activity-center :notifications-unread :data])))))) :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 (deftest notifications-fetch-test
(testing "fetches first page" (testing "fetches first page"
(rf-test/run-test-sync (rf-test/run-test-sync
(setup) (setup)
(let [spy-queue (atom [])] (let [spy-queue (atom [])]
(h/stub-fx-with-callbacks ::json-rpc/call (h/stub-fx-with-callbacks
:on-success (constantly {:cursor "10" ::json-rpc/call
:notifications [{:chatId "0x1"}]})) :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) (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 (= :unread (get-in (h/db) [:activity-center :filter :status])))
(is (nil? (get-in (h/db) [:activity-center :notifications-unread :loading?]))) (is (= "" (get-in @spy-queue [0 :args 0 :params 0]))
(is (= "10" (get-in (h/db) [:activity-center :notifications-unread :cursor]))) "Should be called with empty cursor when fetching first page")
(is (= "" (get-in @spy-queue [0 :args 0 :params 0]))) (is (= {c/activity-center-notification-type-one-to-one-chat
(is (= [{:chat-id "0x1"}] {:unread {:cursor "10"
(->> (get-in (h/db) [:activity-center :notifications-unread :data]) :data [{:chat-id "0x9"
(map #(select-keys % [:chat-id])))))))) :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" (testing "does not fetch next page when pagination cursor reached the end"
(rf-test/run-test-sync (rf-test/run-test-sync
(setup) (setup)
(let [spy-queue (atom [])] (let [spy-queue (atom [])]
(h/spy-fx spy-queue ::json-rpc/call) (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 :filter :status]
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :cursor] ""]) :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]) (rf/dispatch [:activity-center.notifications/fetch-next-page])
@ -104,30 +290,54 @@
(rf-test/run-test-sync (rf-test/run-test-sync
(setup) (setup)
(let [spy-queue (atom [])] (let [spy-queue (atom [])]
(h/stub-fx-with-callbacks ::json-rpc/call (h/stub-fx-with-callbacks
:on-success (constantly {:cursor "" ::json-rpc/call
:notifications [{:chatId "0x1"}]})) :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) (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 :filter :status]
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :cursor] "10"]) :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]) (rf/dispatch [:activity-center.notifications/fetch-next-page])
(is (= "wakuext_unreadActivityCenterNotifications" (get-in @spy-queue [0 :args 0 :method]))) (is (= "wakuext_unreadActivityCenterNotifications" (get-in @spy-queue [0 :args 0 :method])))
(is (= "10" (get-in @spy-queue [0 :args 0 :params 0]))) (is (= "10" (get-in @spy-queue [0 :args 0 :params 0]))
(is (= "" (get-in (h/db) [:activity-center :notifications-unread :cursor]))) "Should be called with current cursor")
(is (= [{:chat-id "0x1"}] (is (= {c/activity-center-notification-type-mention
(->> (get-in (h/db) [:activity-center :notifications-unread :data]) {:unread {:cursor ""
(map #(select-keys % [:chat-id])))))))) :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" (testing "does not fetch next page while it is still loading"
(rf-test/run-test-sync (rf-test/run-test-sync
(setup) (setup)
(let [spy-queue (atom [])] (let [spy-queue (atom [])]
(h/spy-fx spy-queue ::json-rpc/call) (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 :filter :status]
(rf/dispatch [:test/assoc-in [:activity-center :notifications-read :cursor] "10"]) :read])
(rf/dispatch [:test/assoc-in [:activity-center :notifications-read :loading?] true]) (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]) (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/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-event-fx spy-queue :activity-center.notifications/fetch-error)
(h/spy-fx spy-queue ::json-rpc/call) (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 :filter :status]
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :cursor] ""]) :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]) (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 (is (= [:activity-center.notifications/fetch-error
:notifications-unread c/activity-center-notification-type-one-to-one-chat
:unread
:fake-error] :fake-error]
(:args (last @spy-queue)))))))) (:args (last @spy-queue))))))))

View File

@ -176,6 +176,7 @@
(def ^:const docs-link "https://status.im/docs/") (def ^:const docs-link "https://status.im/docs/")
(def ^:const principles-link "https://our.status.im/our-principles/") (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-one-to-one-chat 1)
(def ^:const activity-center-notification-type-private-group-chat 2) (def ^:const activity-center-notification-type-private-group-chat 2)
(def ^:const activity-center-notification-type-mention 3) (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 5)
(def ^:const activity-center-notification-type-contact-request-retracted 6) (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-unknown 0)
(def ^:const visibility-status-automatic 1) (def ^:const visibility-status-automatic 1)
(def ^:const visibility-status-dnd 2) (def ^:const visibility-status-dnd 2)

View File

@ -6,35 +6,36 @@
[clojure.string :as string])) [clojure.string :as string]))
(re-frame/reg-sub (re-frame/reg-sub
:activity-center/notifications-read :activity-center/notifications
(fn [db] :<- [:activity-center]
(get-in db [:activity-center :notifications-read :data]))) (fn [activity-center]
(:notifications activity-center)))
(re-frame/reg-sub (re-frame/reg-sub
:activity-center/notifications-unread :activity-center/filter-status
(fn [db] :<- [:activity-center]
(get-in db [:activity-center :notifications-unread :data]))) (fn [activity-center]
(get-in activity-center [:filter :status])))
(re-frame/reg-sub (re-frame/reg-sub
:activity-center/current-status-filter :activity-center/filter-type
(fn [db] :<- [:activity-center]
(get-in db [:activity-center :current-status-filter]))) (fn [activity-center]
(get-in activity-center [:filter :type] constants/activity-center-notification-type-no-type)))
(re-frame/reg-sub (re-frame/reg-sub
:activity-center/status-filter-unread-enabled? :activity-center/filtered-notifications
:<- [:activity-center/current-status-filter] :<- [:activity-center/filter-type]
(fn [current-status-filter] :<- [:activity-center/filter-status]
(= :unread current-status-filter))) :<- [:activity-center/notifications]
(fn [[filter-type filter-status notifications]]
(get-in notifications [filter-type filter-status :data])))
(re-frame/reg-sub (re-frame/reg-sub
:activity-center/notifications-per-read-status :activity-center/filter-status-unread-enabled?
:<- [:activity-center/notifications-read] :<- [:activity-center/filter-status]
:<- [:activity-center/notifications-unread] (fn [filter-status]
:<- [:activity-center/status-filter-unread-enabled?] (= :unread filter-status)))
(fn [[notifications-read notifications-unread unread-filter-enabled?]]
(if unread-filter-enabled?
notifications-unread
notifications-read)))
(defn- group-notifications-by-date (defn- group-notifications-by-date
[notifications] [notifications]

View File

@ -228,6 +228,7 @@
(reg-root-key-sub :activity.center/notifications :activity.center/notifications) (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/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/description-error :bug-report/description-error)
(reg-root-key-sub :bug-report/details :bug-report/details) (reg-root-key-sub :bug-report/details :bug-report/details)

View File

@ -1,5 +1,6 @@
(ns status-im.ui.screens.activity-center.views (ns status-im.ui.screens.activity-center.views
(:require [quo.components.animated.pressable :as animation] (:require [quo.components.animated.pressable :as animation]
[quo.design-system.colors :as quo.colors]
[quo.react-native :as rn] [quo.react-native :as rn]
[quo2.components.buttons.button :as button] [quo2.components.buttons.button :as button]
[quo2.components.markdown.text :as text] [quo2.components.markdown.text :as text]
@ -14,9 +15,6 @@
[status-im.utils.datetime :as datetime] [status-im.utils.datetime :as datetime]
[status-im.utils.handlers :refer [<sub >evt]])) [status-im.utils.handlers :refer [<sub >evt]]))
(defonce selected-activity-type
(reagent/atom :activity-type/all))
(defn activity-title (defn activity-title
[{:keys [type]}] [{:keys [type]}]
(case type (case type
@ -32,8 +30,8 @@
[{:keys [type]}] [{:keys [type]}]
(case type (case type
constants/activity-center-notification-type-contact-request constants/activity-center-notification-type-contact-request
:add-user :main-icons2/add-user
:placeholder)) :main-icons2/placeholder))
(defn activity-context (defn activity-context
[{:keys [message last-message type]}] [{:keys [message last-message type]}]
@ -81,7 +79,7 @@
nil)) nil))
(defn activity-pressable (defn activity-pressable
[notification & children] [notification activity]
(case (get-in notification [:message :contact-request-state]) (case (get-in notification [:message :contact-request-state])
constants/contact-request-message-state-accepted constants/contact-request-message-state-accepted
;; NOTE [2022-09-21]: We need to dispatch to ;; NOTE [2022-09-21]: We need to dispatch to
@ -89,14 +87,13 @@
;; `:chat.ui/navigate-to-chat`, otherwise the chat screen looks completely ;; `:chat.ui/navigate-to-chat`, otherwise the chat screen looks completely
;; broken if it has never been opened before for the accepted contact. ;; 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)}])} [animation/pressable {:on-press #(>evt [:contact.ui/send-message-pressed {:public-key (:author notification)}])}
children] activity]
[:<> children])) activity))
(defn render-notification (defn render-notification
[notification index] [notification index]
[rn/view {:flex 1 [rn/view {:margin-top (if (= 0 index) 0 4)
:flex-direction :column :padding-horizontal 20}
:margin-top (if (= 0 index) 0 4)}
[activity-pressable notification [activity-pressable notification
[activity-logs/activity-log [activity-logs/activity-log
(merge {:context (activity-context notification) (merge {:context (activity-context notification)
@ -108,67 +105,108 @@
:unread? (not (:read notification))} :unread? (not (:read notification))}
(activity-buttons notification))]]]) (activity-buttons notification))]]])
(defn notifications-list (defn filter-selector-read-toggle
[] []
(let [notifications (<sub [:activity-center/notifications-per-read-status])] (let [unread-filter-enabled? (<sub [:activity-center/filter-status-unread-enabled?])]
[rn/flat-list {:data notifications
:key-fn :id
:on-end-reached #(>evt [:activity-center.notifications/fetch-next-page])
:render-fn render-notification}]))
(defn filter-selector-read []
(let [unread-filter-enabled? (<sub [:activity-center/status-filter-unread-enabled?])]
;; TODO: Replace the button by a Filter Selector component once available for use. ;; TODO: Replace the button by a Filter Selector component once available for use.
[button/button {:icon true [button/button {:icon true
:type (if unread-filter-enabled? :primary :outline) :type (if unread-filter-enabled? :primary :outline)
:size 32 :size 32
:on-press #(if unread-filter-enabled? :on-press #(>evt [:activity-center.notifications/fetch-first-page
(>evt [:activity-center.notifications/fetch-first-page {:status-filter :read}]) {:filter-status (if unread-filter-enabled?
(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}]))} :read
:unread])) :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 (<sub [:activity-center/filter-type])]
[tabs/scrollable-tabs {:size 32
:style {:padding-left 20}
:fade-end-percentage 0.79
:scroll-on-press? true
:fade-end? true
:on-change #(>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 (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 :reagent-render
(fn [] (fn []
(let [screen-padding 20] (let [notifications (<sub [:activity-center/filtered-notifications])]
[:<> [rn/flat-list {:content-container-style {:flex-grow 1}
[button/button {:icon true :data notifications
:type :grey :empty-component [empty-tab]
:size 32 :header [header]
:style {:margin-vertical 12 :key-fn :id
:margin-left screen-padding} :on-end-reached #(>evt [:activity-center.notifications/fetch-next-page])
:on-press #(>evt [:navigate-back])} :render-fn render-notification
:close] :sticky-header-indices [0]}]))}))
[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]]]))}))